pax_global_header00006660000000000000000000000064150307445330014516gustar00rootroot0000000000000052 comment=675146bd8bce6245d78889f543b5c02a1e3936fe openmw-openmw-0.49.0/000077500000000000000000000000001503074453300144205ustar00rootroot00000000000000openmw-openmw-0.49.0/.clang-format000066400000000000000000000056541503074453300170050ustar00rootroot00000000000000--- 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 openmw-openmw-0.49.0/.clang-tidy000066400000000000000000000006221503074453300164540ustar00rootroot00000000000000Checks: > -*, portability-*, clang-analyzer-*, -clang-analyzer-optin.*, -clang-analyzer-cplusplus.NewDeleteLeaks, -clang-analyzer-cplusplus.NewDelete, -clang-analyzer-core.CallAndMessage, modernize-avoid-bind, readability-identifier-naming WarningsAsErrors: '*' HeaderFilterRegex: '(apps|components)/' CheckOptions: - key: readability-identifier-naming.ConceptCase value: CamelCase openmw-openmw-0.49.0/.editorconfig000066400000000000000000000005501503074453300170750ustar00rootroot00000000000000root = true [*.cpp] indent_style = space indent_size = 4 insert_final_newline = true [*.hpp] indent_style = space indent_size = 4 insert_final_newline = true [*.{glsl,vert,tesc,tese,geom,frag,comp}] indent_style = space indent_size = 4 insert_final_newline = true [{CMakeLists.txt,*.cmake}] indent_style = space indent_size = 4 insert_final_newline = true openmw-openmw-0.49.0/.git-blame-ignore-revs000066400000000000000000000012401503074453300205150ustar00rootroot00000000000000# 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 openmw-openmw-0.49.0/.github/000077500000000000000000000000001503074453300157605ustar00rootroot00000000000000openmw-openmw-0.49.0/.github/workflows/000077500000000000000000000000001503074453300200155ustar00rootroot00000000000000openmw-openmw-0.49.0/.github/workflows/push.yml000066400000000000000000000050621503074453300215220ustar00rootroot00000000000000name: Build and test on: - push - pull_request env: BUILD_TYPE: RelWithDebInfo jobs: Ubuntu: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - 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@v4 - name: Install Building Dependencies run: CI/before_install.macos.sh - name: Prime ccache uses: hendrikmuhs/ccache-action@v1 with: key: ${{ matrix.os }}-${{ env.BUILD_TYPE }} max-size: 1000M - name: Configure run: CI/before_script.macos.sh - name: Build run: CI/macos/build.sh Output-Envs: name: Read .env file and expose it as output runs-on: ubuntu-latest outputs: VCPKG_DEPS_TAG: ${{ env.VCPKG_DEPS_TAG }} BUILD_TYPE: ${{ env.BUILD_TYPE }} steps: - uses: actions/checkout@v4 - run: cat "${{ github.workspace }}/CI/github.env" >> $GITHUB_ENV Windows: needs: - Output-Envs strategy: fail-fast: true matrix: image: - "2019" - "2022" uses: ./.github/workflows/windows.yml with: image: ${{ matrix.image }} vcpkg-deps-tag: ${{ needs.Output-Envs.outputs.VCPKG_DEPS_TAG }} build-type: ${{ needs.Output-Envs.outputs.BUILD_TYPE }} openmw-openmw-0.49.0/.github/workflows/release.yml000066400000000000000000000011371503074453300221620ustar00rootroot00000000000000name: Release on: push: tags: - openmw-** jobs: Output-Envs: name: Read .env file and expose it as output runs-on: ubuntu-latest outputs: VCPKG_DEPS_TAG: ${{ env.VCPKG_DEPS_TAG }} steps: - uses: actions/checkout@v4 - run: cat "${{ github.workspace }}/CI/github.env" >> $GITHUB_ENV Windows: needs: - Output-Envs uses: ./.github/workflows/windows.yml with: image: "2022" vcpkg-deps-tag: ${{ needs.Output-Envs.outputs.VCPKG_DEPS_TAG }} build-type: Release package: true release: true secrets: inherit openmw-openmw-0.49.0/.github/workflows/windows.yml000066400000000000000000000223641503074453300222410ustar00rootroot00000000000000name: Reusable Windows workflow on: workflow_call: inputs: image: description: MSVC image (2019/2022) required: true type: string vcpkg-deps-tag: description: Git tag of our deps required: true type: string build-type: default: RelWithDebInfo type: string package: default: false type: boolean release: default: false type: boolean jobs: Windows: name: windows-${{ inputs.image }} runs-on: windows-${{ inputs.image }} env: archive: FAILEDTODOWNLOAD steps: - name: Install NSIS if: ${{ inputs.package }} run: choco install nsis - uses: actions/checkout@v4 - 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: | $MANIFEST = "vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}.txt" curl --fail --retry 3 -L -o "$MANIFEST" "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/$MANIFEST" $lines = Get-Content "$MANIFEST" $URL = $lines[0] $split = -split $lines[1] $HASH = $split[0] $FILE = $split[1] curl --fail --retry 3 -L -o "$FILE" "$URL" $filehash = Get-FileHash "$FILE" -Algorithm SHA512 if ( $filehash.hash -ne "$HASH" ) { exit 1 } echo "archive=$FILE" >> $env:GITHUB_ENV - name: Extract archived prebuilt vcpkg packages working-directory: ${{ github.workspace }}/deps run: 7z x -y -ovcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }} $env:archive - 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=${{ inputs.build-type }} -D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/scripts/buildsystems/vcpkg.cmake' -D CMAKE_PREFIX_PATH='${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64' ${{ inputs.package && '-D CMAKE_CXX_FLAGS_RELEASE="/O2 /Ob2 /DNDEBUG /Zi"' || '' }} ${{ inputs.package && '-D "CMAKE_EXE_LINKER_FLAGS_RELEASE=/DEBUG /INCREMENTAL:NO"' || '' }} -D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/include/luajit' -D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/lib/lua51.lib' -D BUILD_BENCHMARKS=${{ ! inputs.package }} -D BUILD_COMPONENTS_TESTS=${{ ! inputs.package }} -D BUILD_OPENMW_TESTS=${{ ! inputs.package }} -D BUILD_OPENCS_TESTS=${{ ! inputs.package }} -D OPENMW_USE_SYSTEM_SQLITE3=OFF -D OPENMW_USE_SYSTEM_YAML_CPP=OFF -D OPENMW_LTO_BUILD=ON ${{ inputs.package && '-D "VCREDIST64=$env:VCToolsRedistDir/vc_redist.x64.exe"' || '' }} - name: Build OpenMW run: cmake --build ${{ github.workspace }}/build - name: Copy missing DLLs run: | cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/build cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/build cp ${{ github.workspace }}/deps/vcpkg-x64-${{ inputs.image }}-${{ inputs.vcpkg-deps-tag }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/build - name: Copy Qt DLLs working-directory: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64 run: | cp bin/Qt6Core.dll ${{ github.workspace }}/build cp bin/Qt6Gui.dll ${{ github.workspace }}/build cp bin/Qt6Network.dll ${{ github.workspace }}/build cp bin/Qt6OpenGL.dll ${{ github.workspace }}/build cp bin/Qt6OpenGLWidgets.dll ${{ github.workspace }}/build cp bin/Qt6Widgets.dll ${{ github.workspace }}/build cp bin/Qt6Svg.dll ${{ github.workspace }}/build mkdir ${{ github.workspace }}/build/styles cp plugins/styles/qwindowsvistastyle.dll ${{ github.workspace }}/build/styles mkdir ${{ github.workspace }}/build/platforms cp plugins/platforms/qwindows.dll ${{ github.workspace }}/build/platforms mkdir ${{ github.workspace }}/build/imageformats cp plugins/imageformats/qsvg.dll ${{ github.workspace }}/build/imageformats mkdir ${{ github.workspace }}/build/iconengines cp plugins/iconengines/qsvgicon.dll ${{ github.workspace }}/build/iconengines - name: Create symbol server directory structure working-directory: ${{ github.workspace }}/build run: | ${{ github.workspace }}\CI\Store-Symbols.ps1 Move-Item ${{ github.workspace }}\build\SymStore ${{ github.workspace }} - name: Move pdb files run: | robocopy build pdb *.pdb /MOVE if ($lastexitcode -lt 8) { $global:LASTEXITCODE = $null } - name: Install OpenMW if: ${{ ! inputs.package }} run: cmake --install ${{ github.workspace }}/build --prefix ${{ github.workspace }}/install - name: Package OpenMW if: ${{ inputs.package }} run: | cpack --config "${{ github.workspace }}/build/CPackConfig.cmake" -B "${{ github.workspace }}/install" rm -r -Force "${{ github.workspace }}/install/_CPack_Packages" - name: Remove extra pdb files if: ${{ ! inputs.package }} 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 == "windows-${{ inputs.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 cp install/CI-ID.txt SymStore/CI-ID.txt - name: Store OpenMW archived pdb files uses: actions/upload-artifact@v4 with: name: openmw-windows-${{ inputs.image }}-pdb-${{ github.sha }} path: ${{ github.workspace }}/pdb/* - name: Store OpenMW build artifacts uses: actions/upload-artifact@v4 with: name: openmw-windows-${{ inputs.image }}-${{ github.sha }} path: ${{ github.workspace }}/install/* - name: Store symbol server artifacts uses: actions/upload-artifact@v4 with: name: openmw-windows-${{ inputs.image }}-sym-store-${{ github.sha }} path: ${{ github.workspace }}/SymStore/* - name: Upload to symbol server env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: eu-west-3 if: ${{ env.AWS_ACCESS_KEY_ID != '' && env.AWS_SECRET_ACCESS_KEY != '' && inputs.package }} working-directory: ${{ github.workspace }}/SymStore run: | choco uninstall awscli -y choco install awscli -y --version=2.22.35 aws --version aws --endpoint-url https://rgw.ctrl-c.liu.se s3 sync --size-only --exclude * --include *.ex_ --include *.dl_ --include *.pd_ . s3://openmw-sym - name: Add install directory to PATH shell: bash run: echo '${{ github.workspace }}/install' >> ${GITHUB_PATH} - name: Run components tests if: ${{ ! inputs.package }} run: build/components-tests.exe - name: Run OpenMW tests if: ${{ ! inputs.package }} run: build/openmw-tests.exe - name: Run OpenMW-CS tests if: ${{ ! inputs.package }} run: build/openmw-cs-tests.exe - name: Run detournavigator navmeshtilescache benchmark if: ${{ ! inputs.package }} run: build/openmw_detournavigator_navmeshtilescache_benchmark.exe - name: Run settings access benchmark if: ${{ ! inputs.package }} run: build/openmw_settings_access_benchmark.exe - name: Run esm refid benchmark if: ${{ ! inputs.package }} run: build/openmw_esm_refid_benchmark.exe - name: Create prerelease if: ${{ inputs.release }} uses: softprops/action-gh-release@v2 with: files: ${{ github.workspace }}/install/*.exe prerelease: true openmw-openmw-0.49.0/.gitignore000066400000000000000000000025721503074453300164160ustar00rootroot00000000000000## make CMakeFiles */CMakeFiles CMakeCache.txt cmake_install.cmake Makefile makefile build*/ prebuilt ##windows build process /deps /MSVC* ## doxygen Doxygen ## ides/editors *~ *.kdev4 *.swp *.swo *.kate-swp .cproject .project .settings .directory .idea cmake-build-* files/windows/*.aps .cache/clangd ## qt-creator CMakeLists.txt.user* .vs .vscode ## resources resources /*.cfg /*.desktop /*.install ## binaries /esmtool /openmw /opencs /niftest /bsatool /openmw-cs /openmw-essimporter /openmw-iniimporter /openmw-launcher /openmw-wizard ## generated objects apps/openmw/config.hpp apps/launcher/ui_contentselector.h apps/launcher/ui_settingspage.h apps/opencs/ui_contentselector.h apps/opencs/ui_filedialog.h apps/wizard/qrc_wizard.cxx apps/wizard/ui_componentselectionpage.h apps/wizard/ui_conclusionpage.h apps/wizard/ui_existinginstallationpage.h apps/wizard/ui_importpage.h apps/wizard/ui_installationpage.h apps/wizard/ui_installationtargetpage.h apps/wizard/ui_intropage.h apps/wizard/ui_languageselectionpage.h apps/wizard/ui_methodselectionpage.h 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 *qrc_resources.cxx *__* *ui_datafilespage.h *ui_graphicspage.h *ui_mainwindow.h *ui_playpage.h *.[ao] *.so venv/ ## operating system files .DS_Store Thumbs.db openmw-openmw-0.49.0/.gitlab-ci.yml000066400000000000000000000751731503074453300170710ustar00rootroot00000000000000default: 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 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: - 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 .Ubuntu: extends: .Ubuntu_Image cache: paths: - apt-cache/ - ccache/ stage: build variables: CMAKE_EXE_LINKER_FLAGS: -fuse-ld=mold OPENMW_CXX_FLAGS: "-Werror -Werror=implicit-fallthrough" 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 ./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: tags: - saas-linux-medium-amd64 image: ubuntu:22.04 stage: build rules: - 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 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 - 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="$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++ 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 # Disable -Werror due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562 OPENMW_CXX_FLAGS: "" 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 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: when: always paths: - teal_declarations Ubuntu_GCC_Debug: extends: .Ubuntu cache: 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 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 # Disable -Werror due to https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105562 OPENMW_CXX_FLAGS: "" 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: 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 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: Ubuntu_Static_Deps.ubuntu_22.04.v1 paths: - apt-cache/ - ccache/ - build/extern/fetched/ before_script: - 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 .Ubuntu_Static_Deps_tests: extends: .Ubuntu_Static_Deps cache: 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 Ubuntu_Clang: extends: .Ubuntu before_script: - CI/install_debian_deps.sh clang openmw-deps openmw-deps-dynamic cache: 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: 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 Ubuntu_Clang_tests_Debug: extends: Ubuntu_Clang cache: 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: stage: build rules: - if: $CI_PROJECT_ID == "7107382" cache: paths: - ccache/ script: - CI/before_install.macos.sh - export CCACHE_BASEDIR="$(pwd)" - export CCACHE_DIR="$(pwd)/ccache" - mkdir -pv "${CCACHE_DIR}" - CI/macos/ccache_prep.sh - CI/before_script.macos.sh - CI/macos/build.sh - cd build - for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${DMG_IDENTIFIER}_${CI_COMMIT_REF_NAME##*/}.dmg"; done - | if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then echo "[default]" > ~/.s3cfg echo "access_key = ${AWS_ACCESS_KEY_ID}" >> ~/.s3cfg echo "secret_key = ${AWS_SECRET_ACCESS_KEY}" >> ~/.s3cfg echo "host_base = rgw.ctrl-c.liu.se" >> ~/.s3cfg echo "host_bucket = %(bucket)s.rgw.ctrl-c.liu.se" >> ~/.s3cfg echo "use_https = True" >> ~/.s3cfg artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/" for dmg in *.dmg; do s3cmd put "${dmg}" s3://openmw-artifacts/${artifactDirectory} done fi - ../CI/macos/ccache_save.sh artifacts: paths: - build/OpenMW-*.dmg macOS14_Xcode15_amd64: extends: .MacOS image: macos-14-xcode-15 tags: - saas-macos-medium-m1 cache: key: macOS14_Xcode15_amd64.v2 variables: CCACHE_SIZE: 3G DMG_IDENTIFIER: amd64 MACOS_AMD64: true macOS14_Xcode15_arm64: extends: .MacOS image: macos-14-xcode-15 tags: - saas-macos-medium-m1 cache: key: macOS14_Xcode15_arm64.v1 variables: DMG_IDENTIFIER: arm64 CCACHE_SIZE: 3G .Compress_And_Upload_Symbols_Base: extends: .Ubuntu_Image stage: build variables: GIT_STRATEGY: none script: - apt-get update - apt-get install -y curl gcab unzip - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64-2.22.35.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: - 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 ccache -y - choco install vswhere -y - choco install ninja -y - choco install python -y - choco install awscli -y --version=2.22.35 - 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}" - $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 - 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 - aws --version - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if(!$?) { Exit $LASTEXITCODE } 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} if(!$?) { Exit $LASTEXITCODE } } Push-Location .. ..\CI\Store-Symbols.ps1 -SkipCompress if(!$?) { Exit $LASTEXITCODE } 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt if(!$?) { Exit $LASTEXITCODE } Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - 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(!$?) { Exit $LASTEXITCODE } } - | if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe if(!$?) { Exit $LASTEXITCODE } } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: key: ninja-2022-v12 paths: - ccache - deps - MSVC2022_64_Ninja/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - MSVC2022_64_Ninja/*.log - MSVC2022_64_Ninja/**/*.log variables: 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_Release: extends: - .Windows_Ninja_Base variables: config: "Release" .Windows_Compress_And_Upload_Symbols_Ninja_Release: extends: - .Compress_And_Upload_Symbols_Base needs: - job: "Windows_Ninja_Release" artifacts: true .Windows_Ninja_Release_MultiView: extends: - .Windows_Ninja_Base variables: multiview: "-M" config: "Release" .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: config: "Debug" .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: 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_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: config: "RelWithDebInfo" targets: "get-version" when: manual .Windows_MSBuild_Base: tags: - 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 vswhere -y - choco install python -y - choco install awscli -y --version=2.22.35 - 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}" - 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 - aws --version - | if (Get-ChildItem -Recurse *.pdb) { 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt if(!$?) { Exit $LASTEXITCODE } 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} if(!$?) { Exit $LASTEXITCODE } } Push-Location .. ..\CI\Store-Symbols.ps1 -SkipCompress if(!$?) { Exit $LASTEXITCODE } 7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt if(!$?) { Exit $LASTEXITCODE } Pop-Location Get-ChildItem -Recurse *.pdb | Remove-Item } - 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(!$?) { Exit $LASTEXITCODE } } - | if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe if(!$?) { Exit $LASTEXITCODE } } } after_script: - Get-Volume - Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log cache: key: msbuild-2022-v12 paths: - deps - MSVC2022_64/deps/Qt artifacts: when: always paths: - "*.zip" - "*.log" - 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: config: "Release" .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: config: "Debug" .Windows_Compress_And_Upload_Symbols_MSBuild_Debug: extends: - .Compress_And_Upload_Symbols_Base needs: - job: "Windows_MSBuild_Debug" artifacts: true Windows_MSBuild_RelWithDebInfo: extends: - .Windows_MSBuild_Base variables: 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_Compress_And_Upload_Symbols_MSBuild_RelWithDebInfo: extends: - .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_CacheInit: # currently, Windows jobs for all configs share the same cache key as we only cache the dependencies extends: - .Windows_MSBuild_Base variables: config: "RelWithDebInfo" targets: "get-version" when: manual .Ubuntu_AndroidNDK_arm64-v8a: tags: - 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: Ubuntu__Focal_AndroidNDK_r22b_arm64-v8a.v2 paths: - apt-cache/ - ccache/ - build/extern/fetched/ before_script: - 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}" - CI/before_install.android.sh - CI/before_script.android.sh - cd build - cmake --build . -- -j $(nproc) # - 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 openmw-openmw-0.49.0/.readthedocs.yaml000066400000000000000000000002551503074453300176510ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/source/conf.py python: install: - requirements: docs/requirements.txt build: os: ubuntu-22.04 tools: python: "3.8" openmw-openmw-0.49.0/.resubmitted_merge_requests.txt000066400000000000000000000000501503074453300226730ustar00rootroot000000000000001471 1450 1420 1314 1216 1172 1160 1051 openmw-openmw-0.49.0/AUTHORS.md000066400000000000000000000204351503074453300160730ustar00rootroot00000000000000Contributors ============ The OpenMW project was started in 2008 by Nicolay Korslund. In the course of years many people have contributed to the project. If you feel your name is missing from this list, please add it to `AUTHORS.md`. Programmers ----------- Bret Curtis (psi29a) - Project leader 2019-present Marc Zinnschlag (Zini) - Project leader 2010-2018 Nicolay Korslund - Project leader 2008-2010 scrawl - Top contributor AbduSharif Adam Hogan (aurix) Aesylwinn aegis AHSauge Aleksandar Jovanov Alex Haddad (rainChu) Alex McKibben alexanderkjall Alexander Nadeau (wareya) 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 Artem Kotsynyak (greye) Artem Nykolenko (anikm21) artemutin Arthur Moore (EmperorArthur) Assumeru athile Aussiemon Austin Salgat (Salgat) Ben Shealy (bentsherman) Berulacks Bo Svensson Britt Mathis (galdor557) Capostrophic Carl Maxwell cc9cii 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 Sam Hellawell (cykoder) Dan Vukelich (sanchezman) darkf Dave Corley (S3ctor) David Cernat (davidcernat) Declan Millar (declan-millar) devnexen Dieho Dmitry Shkurskiy (endorph) Douglas Diniz (Dgdiniz) Douglas Mencken (dougmencken) dreamer-dead 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) Gašper Sedej Gijsbert ter Horst (Ghostbird) Gohan1989 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 Jeffrey Haines (Jyby) Jengerer Jiří Kuneš (kunesj) Joe Wilkerson (neuralroberts) Joel Graff (graffy) John Blomberg (fstp) Jordan Ayers Jordan Milne Josquin Frei Josua Grawitter Jules Blok (Armada651) julianko Julien Voisin (jvoisin/ap0) Karl-Felix Glatzer (k1ll) Kevin Poitra (PuppyKevin) Koncord Kurnevsky Evgeny (kurnevsky) Lars Söderberg (Lazaroth) lazydev Léo Peltier Leon Krieg (lkrieg) Leon Saunders (emoose) logzero lohikaarme Lordrea Łukasz Gołębiewski (lukago) Lukasz Gromanowski (lgro) Mads Sandvei (Foal) Maksim Eremenko (Max Yari) Marc Bouvier (CramitDeFrog) Marcin Hulist (Gohan) Mark Siewert (mark76) Marco Melletti (mellotanica) Marco Schulze Martin Otto (MAtahualpa) Mateusz Kołaczek (PL_kolek) Mateusz Malisz (malice) Max Henzerling (SaintMercury) megaton Mehdi Yousfi-Monod (mym) Michael Hogan (Xethik) Michael Mc Donnell Michael Papageorgiou (werdanith) Michael Stopa (Stomy) Michał Ściubidło (mike-sc) Michał Bień (Glorf) Michał Moroz (dragonee) Miloslav Číž (drummyfish) Miroslav Puda (pakanek) MiroslavR Mitchell Schwitzer (schwitzerm) Mitten.O naclander Narmo Nat Meo (Utopium) Nathan Jeffords (blunted2night) NeveHanter 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) pchan3 Perry Hugh Petr Mikheev (ptmikheev) Phillip Andrews (PhillipAnd) Pi03k Pieter van der Kloet (pvdk) pkubik PLkolek PlutonicOverkill Qlonever Radu-Marius Popovici (rpopovici) Rafael Moura (dhustkoder) Randy Davin (Kindi) rdimesio rexelion riothamus Rob Cutmore (rcutmore) Robert MacGregor (Ragora) Rohit Nirmal Roman Melnik (Kromgart) Roman Proskuryakov (kpp) Roman Siromakha (elsid) Sandy Carter (bwrsandman) Scott Howard (maqifrnswa) Sebastian Wick (swick) Sergey Fukanchik Sergey Shambir (sergey-shambir) sergoz ShadowRadiance Shihan42 Siimacore Simon Meulenbeek (simonmb) sir_herrbatka smbas Sophie Kirschner (pineapplemachine) spycrab Stefan Galowicz (bogglez) Stanislav Bobrov (Jiub) Stanislaw Halik (sthalik) Star-Demon stil-t svaante Sylvain Thesnieres (Garvek) t6 terrorfisch Tess (tescoShoppah) thegriglat Thomas Luppi (Digmaster) Tim Hagberg (hazardMan) 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) Wolfgang Lieff xyzz Yohaulticetl Yuri Krupenin Yury Stepovikov zelurker Documentation ------------- Adam Bowen (adamnbowen) Alejandro Sanchez (HiPhish) Bodillium Bret Curtis (psi29a) Cramal David Walley (Loriel) Diego Crespo Joakim Berg (lysol90) Ryan Tucker (Ravenwing) sir_herrbatka David Nagy (zuzaman) Packagers --------- Alexander Olofsson (Ananace) - Windows and Flatpak Alexey Sokolov (DarthGandalf) - Gentoo Linux Bret Curtis (psi29a) - Debian and Ubuntu Linux Edmondo Tommasina (edmondo) - Gentoo Linux Julian Ospald (hasufell) - Gentoo Linux Karl-Felix Glatzer (k1ll) - Linux Binaries Kenny Armstrong (artorius) - Fedora Linux Nikolay Kasyanov (corristo) - Mac OS X Sandy Carter (bwrsandman) - Arch Linux Public Relations and Translations --------------------------------- Artem Kotsynyak (greye) - Russian News Writer Dawid Lakomy (Vedyimyn) - Polish News Writer ElderTroll - Release Manager Jim Clauwaert (Zedd) - Public Outreach juanmnzsk8 - Spanish News Writer Julien Voisin (jvoisin/ap0) - French News Writer Kingpix - Italian News Writer Lukasz Gromanowski (lgro) - English News Writer Martin Otto (Atahualpa) - Podcaster, Public Outreach, German Translator Mickey Lyle (raevol) - Release Manager Nekochan - English News Writer penguinroad - Indonesian News Writer Pithorn - Chinese News Writer sir_herrbatka - Polish News Writer spyboot - German Translator Tom Koenderink (Okulo) - English News Writer Website ------- Lukasz Gromanowski (Lgro) - Website Administrator Ryan Sardonic (Wry) - Wiki Editor sir_herrbatka - Forum Administrator Formula Research ---------------- Hrnchamd Epsilon fragonard Greendogo HiPhish modred11 Myckel natirips Sadler Artwork ------- Necrod - OpenMW Logo Mickey Lyle (raevol) - Wordpress Theme Tom Koenderink (Okulo), SirHerrbatka, crysthala, Shnatsel, Lamoot - OpenMW Editor Icons Additional Credits ------------------ In this section we would like to thank people not part of OpenMW for their work. Thanks to Maxim Nikolaev, for allowing us to use his excellent Morrowind fan-art on our website and in other places. Thanks to DokterDume, for kindly providing us with the Moon and Star logo, used as the application icon and project logo. Thanks to Kevin Ryan, for creating the icon used for the Data Files tab of the OpenMW Launcher. Thanks to DejaVu team, for their DejaVuLGCSansMono fontface, see DejaVuFontLicense.txt for their license terms. openmw-openmw-0.49.0/CHANGELOG.md000066400000000000000000007224461503074453300162500ustar00rootroot000000000000000.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 #4289: Script compilation fails if an NPC deleted in a content file is disabled 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 #4710: Object tooltips don't always stick to the top of the object 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 #6657: Distant terrain tiles become black when using FWIW mod Bug #6661: Saved games that have no preview screenshot cause issues or crashes Bug #6665: The kobolds in the skyrim: home of the nords mod are oversized 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 #7013: Local map rendering in some cells is broken Bug #7034: Misc items defined in one content file are not treated as keys if another content file uses them as such Bug #7040: Incorrect rendering order for Rebirth's Stormfang 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 #7051: Collada animated character models are optimized out of the collision box instance with object paging 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 #7145: Normals passed to post-processing shaders are broken Bug #7146: Debug draw for normals is wrong 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 #7351: Unsupported MSAA level fallback wrecks GL context extension checks Bug #7353: Normal Map Crashes with Starwind Assets in TES3MP and OpenMW 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 #7469: Reloading lua with a orphaned lua UI element causes crash 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 #7531: Some bitmap font glyph substitutes are erroneous or missing Bug #7535: Bookart paths for textures in OpenMW vs vanilla Morrowind Bug #7548: Actors cannot open doors that were teleported from a different cell 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 #7582: Skill specializations are hardcoded in character creation 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 #7627: Сrash at the start 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 #7696: Freeze in CompositeMapRenderer::drawImplementation 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 #7728: Fatal Error at Startup 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 #7744: Player base record cannot have weapons in the inventory 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 #7787: Crashing when loading a saved game (not always though) 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 #7891: Launcher Reverts 8k Shadows to default 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 #7912: Lua: castRenderingRay fails to hit height map Bug #7943: Using "addSoulGem" and "dropSoulGem" commands to creatures works only with "Weapon & Shield" flagged ones Bug #7950: Crash in MWPhysics::PhysicsTaskScheduler::removeCollisionObject 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 #8099: Reaching Lua memory limit leads to a crash Bug #8124: Normal weapon resistance is applied twice for NPCs Bug #8132: Actors without hello responses turn to face the player Bug #8171: Items with more than 100% health can be repaired Bug #8172: Openmw-cs crashes when viewing `Dantooine, Sea` Bug #8187: Intervention effects should use Chebyshev distance to determine the closest marker Bug #8189: The import tab in the launcher doesn't remember the checkbox selection Bug #8191: NiRollController does not work for sheath meshes Bug #8206: Moving away from storm wind origin should make you faster Bug #8207: Using hand-to-hand while sneaking plays the critical hit sound when the target is not getting hurt Bug #8208: The launcher's view distance option's minimum value isn't capped to Vanilla's minimum Bug #8223: Ghosts don't move while spellcasting Bug #8231: AGOP doesn't like NiCollisionSwitch Bug #8237: Non-bipedal creatures should *not* use spellcast equip/unequip animations Bug #8252: Plugin dependencies are not required to be loaded Bug #8295: Post-processing chain is case-sensitive Bug #8299: Crash while smoothing landscape Bug #8364: Crash when clicking scrollbar without handle (divide by zero) Bug #8378: Korean bitmap fonts are unusable Bug #8439: Creatures without models can crash the game Bug #8441: Freeze when using video main menu replacers Bug #8445: Launcher crashes on exit when cell name loading thread is still running Bug #8462: Crashes when resizing the window on macOS Bug #8465: Blue screen w/ antialiasing and post-processing on macOS Bug #8503: Camera does not handle NaN gracefully Bug #8541: Lua: util.color:asHex produces wrong output for some colors Bug #8567: Token replacement does not work via CLI and relative paths passed via the command line are not relative to the CWD Bug #8576: Crash on exit when unresolving containers with scripted items Feature #1415: Infinite fall failsafe Feature #2566: Handle NAM9 records for manual cell references Feature #3501: OpenMW-CS: Instance Editing - Shortcuts for axial locking 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 #6505: UTF-8 support in Lua scripts 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 #6823: Animation layering for osgAnimation formats 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 #7091: Allow passing `initData` to the :addScript call 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 #7160: Editor: Moving the Response column of Topicinfos in a better place Feature #7161: OpenMW-CS: Make adding and filtering TopicInfos easier Feature #7180: Rename water_nm file and move it to the vfs Feature #7194: Ori to show texture paths Feature #7214: Searching in the in-game console Feature #7245: Expose the argument `cancelOther` of `AiSequence::stack` to Lua Feature #7248: Searching in the console with regex and toggleable case-sensitivity Feature #7318: Ability to disable water culling 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 #7538: Lua API for advancing skills 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 #7788: [Lua] Add ignore option to nearby.castRenderingRay 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 #8038: (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 #8092: Lua - Vector swizzling Feature #8109: Expose commitCrime to Lua API Feature #8130: Launcher: Add the ability to open a selected data directory in the file browser Feature #8145: Starter spell flag is not exposed Feature #8286: Launcher: Preserve semantically identical openmw.cfg Feature #8287: Launcher: Special handling for comma in openmw.cfg entries is unintuitive and should be removed Task #5859: User openmw-cs.cfg has comment talking about settings.cfg Task #5896: Do not use deprecated MyGUI properties Task #6085: Replace boost::filesystem with std::filesystem Task #6149: Dehardcode Lua API_REVISION 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 #7182: FFMpeg 5.1.1+ support Task #7394: Drop support for --fs-strict Task #7720: Drop 360-degree screenshot support Task #8141: Merge Instance Drop Modes Task #8214: Drop script blacklisting functionality 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 #6097: Level Progress Tooltip Sometimes Not Updated 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 #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 ------ Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path Bug #1901: Actors colliding behaviour is different from vanilla Bug #1952: Incorrect particle lighting 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 #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 Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects Bug #3789: Crash in visitEffectSources while in battle Bug #3862: Random container contents behave differently than vanilla Bug #3929: Leveled list merchant containers respawn on barter Bug #4021: Attributes and skills are not stored as floats 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 #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 Bug #4447: Actor collision capsule shape allows looking through some walls Bug #4465: Collision shape overlapping causes twitching Bug #4476: Abot Gondoliers: player hangs in air during scenic travel Bug #4568: Too many actors in one spot can push other actors out of bounds Bug #4623: Corprus implementation is incorrect Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem Bug #4765: Data race in ChunkManager -> Array::setBinding Bug #4774: Guards are ignorant of an invisible player that tries to attack them Bug #5026: Data races with rain intensity uniform set by sky and used by water Bug #5101: Hostile followers travel with the player Bug #5108: Savegame bloating due to inefficient fog textures format Bug #5165: Active spells should use real time intead of timestamps Bug #5300: NPCs don't switch from torch to shield when starting combat Bug #5358: ForceGreeting always resets the dialogue window completely Bug #5363: Enchantment autocalc not always 0/1 Bug #5364: Script fails/stops if trying to startscript an unknown script Bug #5367: Selecting a spell on an enchanted item per hotkey always plays the equip sound Bug #5369: Spawnpoint in the Grazelands doesn't produce oversized creatures Bug #5370: Opening an unlocked but trapped door uses the key Bug #5384: OpenMW-CS: Deleting an instance requires reload of scene window to show in editor Bug #5387: Move/MoveWorld don't update the object's cell properly Bug #5391: Races Redone 1.2 bodies don't show on the inventory Bug #5397: NPC greeting does not reset if you leave + reenter area Bug #5400: OpenMW-CS: Verifier checks race of non-skin bodyparts Bug #5403: Enchantment effect doesn't show on an enemy during death animation Bug #5415: Environment maps in ebony cuirass and HiRez Armors Indoril cuirass don't work Bug #5416: Junk non-node records before the root node are not handled gracefully Bug #5422: The player loses all spells when resurrected Bug #5423: Guar follows actors too closely Bug #5424: Creatures do not headtrack player Bug #5425: Poison effect only appears for one frame Bug #5427: GetDistance unknown ID error is misleading Bug #5431: Physics performance degradation after a specific number of actors on a scene Bug #5435: Enemies can't hurt the player when collision is off Bug #5441: Enemies can't push a player character when in critical strike stance Bug #5451: Magic projectiles don't disappear with the caster Bug #5452: Autowalk is being included in savegames Bug #5469: Local map is reset when re-entering certain cells Bug #5472: Mistify mod causes CTD in 0.46 on Mac Bug #5473: OpenMW-CS: Cell border lines don't update properly on terrain change Bug #5479: NPCs who should be walking around town are standing around without walking Bug #5484: Zero value items shouldn't be able to be bought or sold for 1 gold Bug #5485: Intimidate doesn't increase disposition on marginal wins Bug #5490: Hits to carried left slot aren't redistributed if there's no shield equipped Bug #5499: Faction advance is available when requirements not met Bug #5502: Dead zone for analogue stick movement is too small Bug #5507: Sound volume is not clamped on ingame settings update Bug #5525: Case-insensitive search in the inventory window does not work with non-ASCII characters Bug #5531: Actors flee using current rotation by axis x Bug #5539: Window resize breaks when going from a lower resolution to full screen resolution Bug #5548: Certain exhausted topics can be highlighted again even though there's no new dialogue Bug #5557: Diagonal movement is noticeably slower with analogue stick Bug #5588: Randomly clicking on the journal's right-side page when it's empty shows random topics Bug #5603: Setting constant effect cast style doesn't correct effects view Bug #5604: Only one valid NIF root node is loaded from a single file Bug #5611: Usable items with "0 Uses" should be used only once Bug #5619: Input events are queued during save loading Bug #5622: Can't properly interact with the console when in pause menu Bug #5627: Bookart not shown if it isn't followed by
statement Bug #5633: Damage Spells in effect before god mode is enabled continue to hurt the player character and can kill them Bug #5639: Tooltips cover Messageboxes 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 #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 Bug #5688: Water shader broken indoors with enable indoor shadows = false Bug #5695: ExplodeSpell for actors doesn't target the ground Bug #5703: OpenMW-CS menu system crashing on XFCE Bug #5706: AI sequences stop looping after the saved game is reloaded Bug #5713: OpenMW-CS: Collada models are corrupted in Qt-based scene view Bug #5731: OpenMW-CS: skirts are invisible on characters 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 #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 Bug #5836: OpenMW dialogue/greeting/voice filter doesn't accept negative Ai values for NPC's hello, alarm, fight, and flee Bug #5838: Local map and other menus become blank in some locations while playing Wizards' Islands mod. Bug #5840: GetSoundPlaying "Health Damage" doesn't play when NPC hits target with shield effect ( vanilla engine behavior ) Bug #5841: Can't Cast Zero Cost Spells When Magicka is < 0 Bug #5869: Guards can initiate arrest dialogue behind locked doors Bug #5871: The console appears if you type the Russian letter "Ё" in the name of the enchantment Bug #5877: Effects appearing with empty icon Bug #5899: Visible modal windows and dropdowns crashing game on exit Bug #5902: NiZBufferProperty is unable to disable the depth test Bug #5906: Sunglare doesn't work with Mesa drivers and AMD GPUs Bug #5912: ImprovedBound mod doesn't work Bug #5914: BM: The Swimmer can't reach destination Bug #5923: Clicking on empty spaces between journal entries might show random topics Bug #5934: AddItem command doesn't accept negative values Bug #5975: NIF controllers from sheath meshes are used Bug #5991: Activate should always be allowed for inventory items Bug #5995: NiUVController doesn't calculate the UV offset properly Bug #6007: Crash when ending cutscene is playing Bug #6016: Greeting interrupts Fargoth's sneak-walk Bug #6022: OpenMW-CS: Terrain selection is not updated when undoing/redoing terrain changes Bug #6023: OpenMW-CS: Clicking on a reference in "Terrain land editing" mode discards corresponding select/edit action Bug #6028: Particle system controller values are incorrectly used Bug #6035: OpenMW-CS: Circle brush in "Terrain land editing" mode sometimes includes vertices outside its radius 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 Feature #4977: Use the "default icon.tga" when an item's icon is not found Feature #5043: Head Bobbing Feature #5199: OpenMW-CS: Improve scene view colors Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines Feature #5456: Basic collada animation support Feature #5457: Realistic diagonal movement Feature #5486: Fixes trainers to choose their training skills based on their base skill points Feature #5500: Prepare enough navmesh tiles before scene loading ends Feature #5511: Add in game option to toggle HRTF support in OpenMW 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 Feature #5610: Actors movement should be smoother Feature #5642: Ability to attach arrows to actor skeleton instead of bow mesh Feature #5649: Skyrim SE compressed BSA format support Feature #5672: Make stretch menu background configuration more accessible Feature #5692: Improve spell/magic item search to factor in magic effect names Feature #5730: Add graphic herbalism option to the launcher and documents Feature #5771: ori command should report where a mesh is loaded from and whether the x version is used. Feature #5813: Instanced groundcover support 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 0.46.0 ------ Bug #1515: Opening console masks dialogue, inventory menu Bug #1933: Actors can have few stocks of the same item 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: [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 Bug #3282: Unintended behaviour when assigning F3 and Windows keys Bug #3550: Companion from mod attacks the air after combat has ended Bug #3609: Items from evidence chest are not considered to be stolen if player is allowed to use the chest Bug #3623: Display scaling breaks mouse recognition Bug #3725: Using script function in a non-conditional expression breaks script compilation Bug #3733: Normal maps are inverted on mirrored UVs Bug #3765: DisableTeleporting makes Mark/Recall/Intervention effects undetectable Bug #3778: [Mod] Improved Thrown Weapon Projectiles - weapons have wrong transformation during throw animation Bug #3812: Wrong multiline tooltips width when word-wrapping is enabled Bug #3894: Hostile spell effects not detected/present on first frame of OnPCHitMe Bug #3977: Non-ASCII characters in object ID's are not supported 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 #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 Bug #4276: Resizing character window differs from vanilla Bug #4284: ForceSneak behaviour is inconsistent if the target has AiWander package Bug #4329: Removed birthsign abilities are restored after reloading the save Bug #4341: Error message about missing GDB is too vague Bug #4383: Bow model obscures crosshair when arrow is drawn Bug #4384: Resist Normal Weapons only checks ammunition for ranged weapons Bug #4411: Reloading a saved game while falling prevents damage in some cases Bug #4449: Value returned by GetWindSpeed is incorrect Bug #4456: AiActivate should not be cancelled after target activation Bug #4493: If the setup doesn't find what it is expecting, it fails silently and displays the requester again instead of letting the user know what wasn't found. Bug #4523: "player->ModCurrentFatigue -0.001" in global script does not cause the running player to fall Bug #4540: Rain delay when exiting water Bug #4594: Actors without AI packages don't use Hello dialogue Bug #4598: Script parser does not support non-ASCII characters Bug #4600: Crash when no sound output is available or --no-sound is used. Bug #4601: Filtering referenceables by gender is broken Bug #4639: Black screen after completing first mages guild mission + training Bug #4650: Focus is lost after pressing ESC in confirmation dialog inside savegame dialog Bug #4680: Heap corruption on faulty esp Bug #4701: PrisonMarker record is not hardcoded like other markers Bug #4703: Editor: it's possible to preview levelled list records Bug #4705: Editor: unable to open exterior cell views from Instances table Bug #4714: Crash upon game load in the repair menu while the "Your repair failed!" message is active Bug #4715: "Cannot get class of an empty object" exception after pressing ESC in the dialogue mode Bug #4720: Inventory avatar has shield with two-handed weapon during [un]equipping animation Bug #4723: ResetActors command works incorrectly Bug #4736: LandTexture records overrides do not work Bug #4745: Editor: Interior cell lighting field values are not displayed as colors Bug #4746: Non-solid player can't run or sneak Bug #4747: Bones are not read from X.NIF file for NPC animation Bug #4748: Editor: Cloned, moved, added instances re-use RefNum indices Bug #4750: Sneaking doesn't work in first person view if the player is in attack ready state Bug #4756: Animation issues with VAOs Bug #4757: Content selector: files can be cleared when there aren't any files to clear Bug #4768: Fallback numerical value recovery chokes on invalid arguments Bug #4775: Slowfall effect resets player jumping flag Bug #4778: Interiors of Illusion puzzle in Sotha Sil Expanded mod is broken Bug #4783: Blizzard behavior is incorrect Bug #4787: Sneaking makes 1st person walking/bobbing animation super-slow Bug #4797: Player sneaking and running stances are not accounted for when in air Bug #4800: Standing collisions are not updated immediately when an object is teleported without a cell change Bug #4802: You can rest before taking falling damage from landing from a jump Bug #4803: Stray special characters before begin statement break script compilation Bug #4804: Particle system with the "Has Sizes = false" causes an exception Bug #4805: NPC movement speed calculations do not take race Weight into account Bug #4810: Raki creature broken in OpenMW Bug #4813: Creatures with known file but no "Sound Gen Creature" assigned use default sounds Bug #4815: "Finished" journal entry with lower index doesn't close journal, SetJournalIndex closes journal Bug #4820: Spell absorption is broken Bug #4823: Jail progress bar works incorrectly Bug #4826: Uninitialized memory in unit test Bug #4827: NiUVController is handled incorrectly Bug #4828: Potion looping effects VFX are not shown for NPCs Bug #4837: CTD when a mesh with NiLODNode root node with particles is loaded Bug #4841: Russian localization ignores implicit keywords Bug #4844: Data race in savegame loading / GlobalMap render Bug #4847: Idle animation reset oddities Bug #4851: No shadows since switch to OSG Bug #4860: Actors outside of processing range visible for one frame after spawning Bug #4867: Arbitrary text after local variable declarations breaks script compilation Bug #4876: AI ratings handling inconsistencies Bug #4877: Startup script executes only on a new game start Bug #4879: SayDone returns 0 on the frame Say is called Bug #4888: Global variable stray explicit reference calls break script compilation Bug #4896: Title screen music doesn't loop Bug #4902: Using scrollbars in settings causes resolution to change Bug #4904: Editor: Texture painting with duplicate of a base-version texture Bug #4911: Editor: QOpenGLContext::swapBuffers() warning with Qt5 Bug #4916: Specular power (shininess) material parameter is ignored when shaders are used. Bug #4918: Abilities don't play looping VFX when they're initially applied Bug #4920: Combat AI uses incorrect invisibility check Bug #4922: Werewolves can not attack if the transformation happens during attack Bug #4927: Spell effect having both a skill and an attribute assigned is a fatal error Bug #4932: Invalid records matching when loading save with edited plugin Bug #4933: Field of View not equal with Morrowind Bug #4938: Strings from subrecords with actually empty headers can't be empty Bug #4942: Hand-to-Hand attack type is chosen randomly when "always use best attack" is turned off Bug #4945: Poor random magic magnitude distribution Bug #4947: Player character doesn't use lip animation Bug #4948: Footstep sounds while levitating on ground level Bug #4952: Torches held by NPCs flicker too quickly Bug #4961: Flying creature combat engagement takes z-axis into account Bug #4963: Enchant skill progress is incorrect 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 #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 Bug #4984: "Friendly hits" feature should be used only for player's followers Bug #4989: Object dimension-dependent VFX scaling behavior is inconsistent Bug #4990: Dead bodies prevent you from hitting Bug #4991: Jumping occasionally takes too much fatigue Bug #4999: Drop instruction behaves differently from vanilla Bug #5001: Possible data race in the Animation::setAlpha() Bug #5004: Werewolves shield their eyes during storm Bug #5012: "Take all" on owned container generates a messagebox per item Bug #5018: Spell tooltips don't support purely negative magnitudes Bug #5025: Data race in the ICO::setMaximumNumOfObjectsToCompilePerFrame() Bug #5028: Offered price caps are not trading-specific Bug #5038: Enchanting success chance calculations are blatantly wrong Bug #5047: # in cell names sets color Bug #5050: Invalid spell effects are not handled gracefully Bug #5055: Mark, Recall, Intervention magic effect abilities have no effect when added and removed in the same frame Bug #5056: Calling Cast function on player doesn't equip the spell but casts it Bug #5059: Modded animation with combined attack keys always does max damage and can double damage Bug #5060: Magic effect visuals stop when death animation begins instead of when it ends Bug #5063: Shape named "Tri Shadow" in creature mesh is visible if it isn't hidden Bug #5067: Ranged attacks on unaware opponents ("critical hits") differ from the vanilla engine Bug #5069: Blocking creatures' attacks doesn't degrade shields Bug #5073: NPCs open doors in front of them even if they don't have to Bug #5074: Paralyzed actors greet the player Bug #5075: Enchanting cast style can be changed if there's no object Bug #5078: DisablePlayerLooking is broken Bug #5081: OpenMW-CS: Apparatus type "Alembic" is erroneously named "Albemic" Bug #5082: Scrolling with controller in GUI mode is broken Bug #5087: Some valid script names can't be used as string arguments Bug #5089: Swimming/Underwater creatures only swim around ground level Bug #5092: NPCs with enchanted weapons play sound when out of charges Bug #5093: Hand to hand sound plays on knocked out enemies Bug #5097: String arguments can't be parsed as number literals in scripts Bug #5099: Non-swimming enemies will enter water if player is water walking Bug #5103: Sneaking state behavior is still inconsistent Bug #5104: Black Dart's enchantment doesn't trigger at low Enchant levels Bug #5106: Still can jump even when encumbered Bug #5110: ModRegion with a redundant numerical argument breaks script execution Bug #5112: Insufficient magicka for current spell not reflected on HUD icon Bug #5113: Unknown alchemy question mark not centered Bug #5123: Script won't run on respawn Bug #5124: Arrow remains attached to actor if pulling animation was cancelled Bug #5126: Swimming creatures without RunForward animations are motionless during combat Bug #5134: Doors rotation by "Lock" console command is inconsistent Bug #5136: LegionUniform script: can not access local variables Bug #5137: Textures with Clamp Mode set to Clamp instead of Wrap are too dark outside the boundaries Bug #5138: Actors stuck in half closed door Bug #5149: Failing lock pick attempts isn't always a crime Bug #5155: Lock/unlock behavior differs from vanilla Bug #5158: Objects without a name don't fallback to their ID Bug #5159: NiMaterialColorController can only control the diffuse color Bug #5161: Creature companions can't be activated when they are knocked down Bug #5163: UserData is not copied during node cloning Bug #5164: Faction owned items handling is incorrect Bug #5166: Scripts still should be executed after player's death Bug #5167: Player can select and cast spells before magic menu is enabled Bug #5168: Force1stPerson and Force3rdPerson commands are not really force view change Bug #5169: Nested levelled items/creatures have significantly higher chance not to spawn Bug #5175: Random script function returns an integer value Bug #5177: Editor: Unexplored map tiles get corrupted after a file with terrain is saved Bug #5182: OnPCEquip doesn't trigger on skipped beast race attempts to equip something not equippable by beasts Bug #5186: Equipped item enchantments don't affect creatures Bug #5190: On-strike enchantments can be applied to and used with non-projectile ranged weapons Bug #5196: Dwarven ghosts do not use idle animations Bug #5206: A "class does not have NPC stats" error when player's follower kills an enemy with damage spell Bug #5209: Spellcasting ignores race height Bug #5210: AiActivate allows actors to open dialogue and inventory windows Bug #5211: Screen fades in if the first loaded save is in interior cell Bug #5212: AiTravel does not work for actors outside of AI processing range Bug #5213: SameFaction script function is broken Bug #5218: Crash when disabling ToggleBorders Bug #5220: GetLOS crashes when actor isn't loaded Bug #5222: Empty cell name subrecords are not saved Bug #5223: Bow replacement during attack animation removes attached arrow Bug #5226: Reputation should be capped Bug #5229: Crash if mesh controller node has no data node Bug #5239: OpenMW-CS does not support non-ASCII characters in path names Bug #5241: On-self absorb spells cannot be detected Bug #5242: ExplodeSpell behavior differs from Cast behavior Bug #5246: Water ripples persist after cell change Bug #5249: Wandering NPCs start walking too soon after they hello Bug #5250: Creatures display shield ground mesh instead of shield body part Bug #5255: "GetTarget, player" doesn't return 1 during NPC hello Bug #5261: Creatures can sometimes become stuck playing idles and never wander again Bug #5264: "Damage Fatigue" Magic Effect Can Bring Fatigue below 0 Bug #5269: Editor: Cell lighting in resaved cleaned content files is corrupted Bug #5278: Console command Show doesn't fall back to global variable after local var not found Bug #5308: World map copying makes save loading much slower Bug #5313: Node properties of identical type are not applied in the correct order Bug #5326: Formatting issues in the settings.cfg Bug #5328: Skills aren't properly reset for dead actors Bug #5345: Dopey Necromancy does not work due to a missing quote 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 #2229: Improve pathfinding AI Feature #3025: Analogue gamepad movement controls Feature #3442: Default values for fallbacks from ini file Feature #3517: Multiple projectiles enchantment Feature #3610: Option to invert X axis Feature #3871: Editor: Terrain Selection Feature #3893: Implicit target for "set" function in console Feature #3980: In-game option to disable controller Feature #3999: Shift + Double Click should maximize/restore menu size 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 Feature #4360: Improve default controller bindings Feature #4544: Actors movement deceleration Feature #4673: Weapon sheathing Feature #4675: Support for NiRollController Feature #4708: Radial fog support Feature #4730: Native animated containers support Feature #4784: Launcher: Duplicate Content Lists Feature #4812: Support NiSwitchNode Feature #4831: Item search in the player's inventory Feature #4836: Daytime node switch Feature #4840: Editor: Transient terrain change support Feature #4859: Make water reflections more configurable Feature #4882: Support for NiPalette node Feature #4887: Add openmw command option to set initial random seed Feature #4890: Make Distant Terrain configurable Feature #4944: Pause audio when OpenMW is minimized 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 Feature #5010: Native graphics herbalism support Feature #5031: Make GetWeaponType function return different values for tools Feature #5033: Magic armor mitigation for creatures Feature #5034: Make enchanting window stay open after a failed attempt Feature #5036: Allow scripted faction leaving Feature #5046: Gamepad thumbstick cursor speed Feature #5051: Provide a separate textures for scrollbars Feature #5091: Human-readable light source duration Feature #5094: Unix like console hotkeys Feature #5098: Allow user controller bindings Feature #5114: Refresh launcher mod list Feature #5121: Handle NiTriStrips and NiTriStripsData Feature #5122: Use magic glow for enchanted arrows Feature #5131: Custom skeleton bones Feature #5132: Unique animations for different weapon types Feature #5146: Safe Dispose corpse Feature #5147: Show spell magicka cost in spell buying window Feature #5170: Editor: Land shape editing, land selection Feature #5172: Editor: Delete instances/references with keypress in scene window Feature #5193: Shields sheathing Feature #5201: Editor: Show tool outline in scene view, when using editmodes Feature #5219: Impelement TestCells console command Feature #5224: Handle NiKeyframeController for NiTriShape Feature #5274: Editor: Keyboard shortcut to drop objects to ground/obstacle in scene view Feature #5304: Morrowind-style bump-mapping Feature #5311: Support for gyroscopic input (e.g. Android) Feature #5314: Ingredient filter in the alchemy window Task #4686: Upgrade media decoder to a more current FFmpeg API Task #4695: Optimize Distant Terrain memory consumption Task #4789: Optimize cell transitions Task #4721: Add NMake support to the Windows prebuild script 0.45.0 ------ Bug #1875: Actors in inactive cells don't heal from resting Bug #1990: Sunrise/sunset not set correct Bug #2131: Lustidrike's spell misses the player every time Bug #2222: Fatigue's effect on selling price is backwards Bug #2256: Landing sound not playing when jumping immediately after landing Bug #2274: Thin platform clips through player character instead of lifting Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2446: Restore Attribute/Skill should allow restoring drained attributes Bug #2455: Creatures attacks degrade armor Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash Bug #2626: Resurrecting the player does not resume the game Bug #2772: Non-existing class or faction freezes the game Bug #2835: Player able to slowly move when overencumbered Bug #2852: No murder bounty when a player follower commits murder Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit Bug #2872: Tab completion in console doesn't work with explicit reference Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #3049: Drain and Fortify effects are not properly applied on health, magicka and fatigue Bug #3059: Unable to hit with marksman weapons when too close to an enemy Bug #3072: Fatal error on AddItem that has a script containing Equip Bug #3219: NPC and creature initial position tracing down limit is too small Bug #3249: Fixed revert function not updating views properly Bug #3288: TrueType fonts are handled incorrectly Bug #3374: Touch spells not hitting kwama foragers Bug #3486: [Mod] NPC Commands does not work Bug #3533: GetSpellEffects should detect effects with zero duration Bug #3591: Angled hit distance too low Bug #3629: DB assassin attack never triggers creature spawning Bug #3681: OpenMW-CS: Clicking Scripts in Preferences spawns many color pickers Bug #3762: AddSoulGem and RemoveSpell redundant count arguments break script execution Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla Bug #3836: Script fails to compile when command argument contains "\n" Bug #3876: Landscape texture painting is misaligned Bug #3890: Magic light source attenuation is inaccurate Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters Bug #3920: RemoveSpellEffects doesn't remove constant effects Bug #3948: AiCombat moving target aiming uses incorrect speed for magic projectiles Bug #3950: FLATTEN_STATIC_TRANSFORMS optimization breaks animated collision shapes Bug #3993: Terrain texture blending map is not upscaled Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts Bug #4125: OpenMW logo cropped on bugtracker Bug #4215: OpenMW shows book text after last EOL tag Bug #4217: Fixme implementation differs from Morrowind's Bug #4221: Characters get stuck in V-shaped terrain Bug #4230: AiTravel package issues break some Tribunal quests Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality Bug #4251: Stationary NPCs do not return to their position after combat Bug #4260: Keyboard navigation makes persuasion exploitable Bug #4271: Scamp flickers when attacking Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+ Bug #4286: Scripted animations can be interrupted Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4292: CenterOnCell implementation differs from vanilla Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4304: "Follow" not working as a second AI package Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4311: OpenMW does not handle RootCollisionNode correctly Bug #4327: Missing animations during spell/weapon stance switching Bug #4333: Keyboard navigation in containers is not intuitive Bug #4358: Running animation is interrupted when magic mode is toggled Bug #4368: Settings window ok button doesn't have key focus by default Bug #4378: On-self absorb spells restore stats Bug #4393: NPCs walk back to where they were after using ResetActors Bug #4416: Non-music files crash the game when they are tried to be played Bug #4419: MRK NiStringExtraData is handled incorrectly Bug #4426: RotateWorld behavior is incorrect Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2 Bug #4431: "Lock 0" console command is a no-op Bug #4432: Guards behaviour is incorrect if they do not have AI packages Bug #4433: Guard behaviour is incorrect with Alarm = 0 Bug #4451: Script fails to compile when using "Begin, [ScriptName]" syntax Bug #4452: Default terrain texture bleeds through texture transitions Bug #4453: Quick keys behaviour is invalid for equipment Bug #4454: AI opens doors too slow Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches Bug #4460: Script function "Equip" doesn't bypass beast restrictions Bug #4461: "Open" spell from non-player caster isn't a crime Bug #4463: %g format doesn't return more digits Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages Bug #4467: Content selector: cyrillic characters are decoded incorrectly in plugin descriptions Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4470: Non-bipedal creatures with Weapon & Shield flag have inconsistent behaviour Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Bug #4479: "Game" category on Advanced page is getting too long Bug #4480: Segfault in QuickKeysMenu when item no longer in inventory Bug #4489: Goodbye doesn't block dialogue hyperlinks Bug #4490: PositionCell on player gives "Error: tried to add local script twice" Bug #4494: Training cap based off Base Skill instead of Modified Skill Bug #4495: Crossbow animations blending is buggy Bug #4496: SpellTurnLeft and SpellTurnRight animation groups are unused Bug #4497: File names starting with x or X are not classified as animation Bug #4503: Cast and ExplodeSpell commands increase alteration skill Bug #4510: Division by zero in MWMechanics::CreatureStats::setAttribute Bug #4519: Knockdown does not discard movement in the 1st-person mode Bug #4527: Sun renders on water shader in some situations where it shouldn't Bug #4531: Movement does not reset idle animations Bug #4532: Underwater sfx isn't tied to 3rd person camera Bug #4539: Paper Doll is affected by GUI scaling Bug #4543: Picking cursed items through inventory (menumode) makes it disappear Bug #4545: Creatures flee from werewolves Bug #4551: Replace 0 sound range with default range separately Bug #4553: Forcegreeting on non-actor opens a dialogue window which cannot be closed Bug #4557: Topics with reserved names are handled differently from vanilla Bug #4558: Mesh optimizer: check for reserved node name is case-sensitive Bug #4560: OpenMW does not update pinned windows properly Bug #4563: Fast travel price logic checks destination cell instead of service actor cell Bug #4565: Underwater view distance should be limited Bug #4573: Player uses headtracking in the 1st-person mode Bug #4574: Player turning animations are twitchy Bug #4575: Weird result of attack animation blending with movement animations Bug #4576: Reset of idle animations when attack can not be started Bug #4591: Attack strength should be 0 if player did not hold the attack button Bug #4593: Editor: Instance dragging is broken Bug #4597: <> operator causes a compile error Bug #4604: Picking up gold from the ground only makes 1 grabbed Bug #4607: Scaling for animated collision shapes is applied twice Bug #4608: Falling damage is applied twice Bug #4611: Instant magic effects have 0 duration in custom spell cost calculations unlike vanilla Bug #4614: Crash due to division by zero when FlipController has no textures Bug #4615: Flicker effects for light sources are handled incorrectly Bug #4617: First person sneaking offset is not applied while the character is in air Bug #4618: Sneaking is possible while the character is flying Bug #4622: Recharging enchanted items with Soul Gems does not award experience if it fails Bug #4628: NPC record reputation, disposition and faction rank should have unsigned char type Bug #4633: Sneaking stance affects speed even if the actor is not able to crouch Bug #4641: GetPCJumping is handled incorrectly Bug #4644: %Name should be available for all actors, not just for NPCs Bug #4646: Weapon force-equipment messes up ongoing attack animations Bug #4648: Hud thinks that throwing weapons have condition Bug #4649: Levelup fully restores health Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects Bug #4656: Combat AI: back up behaviour is incorrect Bug #4668: Editor: Light source color is displayed as an integer Bug #4669: ToggleCollision should trace the player down after collision being enabled Bug #4671: knownEffect functions should use modified Alchemy skill Bug #4672: Pitch factor is handled incorrectly for crossbow animations Bug #4674: Journal can be opened when settings window is open Bug #4677: Crash in ESM reader when NPC record has DNAM record without DODT one Bug #4678: Crash in ESP parser when SCVR has no variable names Bug #4684: Spell Absorption is additive Bug #4685: Missing sound causes an exception inside Say command Bug #4689: Default creature soundgen entries are not used Bug #4691: Loading bar for cell should be moved up when text is still active at bottom of screen Feature #912: Editor: Add missing icons to UniversalId tables Feature #1221: Editor: Creature/NPC rendering Feature #1617: Editor: Enchantment effect record verifier Feature #1645: Casting effects from objects Feature #2606: Editor: Implemented (optional) case sensitive global search Feature #2787: Use the autogenerated collision box, if the creature mesh has no predefined one Feature #2845: Editor: add record view and preview default keybindings Feature #2847: Content selector: allow to copy the path to a file by using the context menu Feature #3083: Play animation when NPC is casting spell via script Feature #3103: Provide option for disposition to get increased by successful trade Feature #3276: Editor: Search - Show number of (remaining) search results and indicate a search without any results Feature #3641: Editor: Limit FPS in 3d preview window Feature #3703: Ranged sneak attack criticals Feature #4012: Editor: Write a log file if OpenCS crashes Feature #4222: 360° screenshots Feature #4256: Implement ToggleBorders (TB) console command Feature #4285: Support soundgen calls for activators Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts Feature #4345: Add equivalents for the command line commands to Launcher Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically Feature #4444: Per-group KF-animation files support Feature #4466: Editor: Add option to ignore "Base" records when running verifier Feature #4488: Make water shader rougher during rain Feature #4509: Show count of enchanted items in stack in the spells list Feature #4512: Editor: Use markers for lights and creatures levelled lists Feature #4548: Weapon priority: use the actual chance to hit the target instead of weapon skill Feature #4549: Weapon priority: use the actual damage in weapon rating calculations Feature #4550: Weapon priority: make ranged weapon bonus more sensible Feature #4579: Add option for applying Strength into hand to hand damage Feature #4581: Use proper logging system Feature #4624: Spell priority: don't cast hit chance-affecting spells if the enemy is not in respective stance at the moment Feature #4625: Weapon priority: use weighted mean for melee damage rating Feature #4626: Weapon priority: account for weapon speed Feature #4632: AI priority: utilize vanilla AI GMSTs for priority rating Feature #4636: Use sTo GMST in spellmaking menu Feature #4642: Batching potion creation Feature #4647: Cull actors outside of AI processing range Feature #4682: Use the collision box from basic creature mesh if the X one have no collisions Feature #4697: Use the real thrown weapon damage in tooltips and AI Task #2490: Don't open command prompt window on Release-mode builds automatically Task #4545: Enable is_pod string test Task #4605: Optimize skinning Task #4606: Support Rapture3D's OpenAL driver Task #4613: Incomplete type errors when compiling with g++ on OSX 10.9 Task #4621: Optimize combat AI Task #4643: Revise editor record verifying functionality Task #4645: Use constants instead of widely used magic numbers Task #4652: Move call to enemiesNearby() from InputManager::rest() to World::canRest() 0.44.0 ------ Bug #1428: Daedra summoning scripts aren't executed when the item is taken through the inventory Bug #1987: Some glyphs are not supported Bug #2254: Magic related visual effects are not rendered when loading a saved game Bug #2485: Journal alphabetical index doesn't match "Morrowind content language" setting Bug #2703: OnPCHitMe is not handled correctly Bug #2829: Incorrect order for content list consisting of a game file and an esp without dependencies Bug #2841: "Total eclipse" happens if weather settings are not defined. Bug #2897: Editor: Rename "Original creature" field Bug #3278: Editor: Unchecking "Auto Calc" flag changes certain values Bug #3343: Editor: ID sorting is case-sensitive in certain tables Bug #3557: Resource priority confusion when using the local data path as installation root Bug #3587: Pathgrid and Flying Creatures wrong behaviour – abotWhereAreAllBirdsGoing Bug #3603: SetPos should not skip weather transitions Bug #3618: Myar Aranath total conversion can't be started due to capital-case extension of the master file Bug #3638: Fast forwarding can move NPC inside objects Bug #3664: Combat music does not start in dialogue Bug #3696: Newlines are accompanied by empty rectangle glyph in dialogs Bug #3708: Controllers broken on macOS Bug #3726: Items with suppressed activation can be picked up via the inventory menu Bug #3783: [Mod] Abot's Silt Striders 1.16 - silt strider "falls" to ground and glides on floor during travel Bug #3863: Can be forced to not resist arrest if you cast Calm Humanoid on aggroed death warrant guards Bug #3884: Incorrect enemy behavior when exhausted Bug #3926: Installation Wizard places Morrowind.esm after Tribunal/Bloodmoon if it has a later file creation date Bug #4061: Scripts error on special token included in name Bug #4111: Crash when mouse over soulgem with a now-missing soul Bug #4122: Swim animation should not be interrupted during underwater attack Bug #4134: Battle music behaves different than vanilla Bug #4135: Reflecting an absorb spell different from vanilla Bug #4136: Enchanted weapons without "ignore normal weapons" flag don't bypass creature "ignore normal weapons" effect Bug #4143: Antialiasing produces graphical artifacts when used with shader lighting Bug #4159: NPCs' base skeleton files should not be optimized Bug #4177: Jumping/landing animation interference/flickering Bug #4179: NPCs do not face target Bug #4180: Weapon switch sound playing even though no weapon is switched Bug #4184: Guards can initiate dialogue even though you are far above them Bug #4190: Enchanted clothes changes visibility with Chameleon on equip/unequip Bug #4191: "screenshot saved" message also appears in the screenshot image Bug #4192: Archers in OpenMW have shorter attack range than archers in Morrowind Bug #4210: Some dialogue topics are not highlighted on first encounter Bug #4211: FPS drops after minimizing the game during rainy weather Bug #4216: Thrown weapon projectile doesn't rotate Bug #4223: Displayed spell casting chance must be 0 if player doesn't have enough magicka to cast it Bug #4225: Double "Activate" key presses with Mouse and Gamepad. Bug #4226: The current player's class should be default value in the class select menu Bug #4229: Tribunal/Bloodmoon summoned creatures fight other summons Bug #4233: W and A keys override S and D Keys Bug #4235: Wireframe mode affects local map Bug #4239: Quick load from container screen causes crash Bug #4242: Crime greetings display in Journal Bug #4245: Merchant NPCs sell ingredients growing on potted plants they own Bug #4246: Take armor condition into account when calcuting armor rating Bug #4250: Jumping is not as fluid as it was pre-0.43.0 Bug #4252: "Error in frame: FFmpeg exception: Failed to allocate input stream" message spam if OpenMW encounter non-music file in the Music folder Bug #4261: Magic effects from eaten ingredients always have 1 sec duration Bug #4263: Arrow position is incorrect in 3rd person view during attack for beast races Bug #4264: Player in god mode can be affected by some negative spell effects Bug #4269: Crash when hovering the faction section and the 'sAnd' GMST is missing (as in MW 1.0) Bug #4272: Root note transformations are discarded again Bug #4279: Sometimes cells are not marked as explored on the map Bug #4298: Problem with MessageBox and chargen menu interaction order Bug #4301: Optimizer breaks LOD nodes Bug #4308: PlaceAtMe doesn't inherit scale of calling object Bug #4309: Only harmful effects with resistance effect set are resistable Bug #4313: Non-humanoid creatures are capable of opening doors Bug #4314: Rainy weather slows down the game when changing from indoors/outdoors Bug #4319: Collisions for certain meshes are incorrectly ignored Bug #4320: Using mouse 1 to move forward causes selection dialogues to jump selections forward. Bug #4322: NPC disposition: negative faction reaction modifier doesn't take PC rank into account Bug #4328: Ownership by dead actors is not cleared from picked items Bug #4334: Torch and shield usage inconsistent with original game Bug #4336: Wizard: Incorrect Morrowind assets path autodetection Bug #4343: Error message for coc and starting cell shouldn't imply that it only works for interior cells Bug #4346: Count formatting does not work well with very high numbers Bug #4351: Using AddSoulgem fills all soul gems of the specified type Bug #4391: No visual indication is provided when an unavailable spell fails to be chosen via a quick key Bug #4392: Inventory filter breaks after loading a game Bug #4405: No default terrain in empty cells when distant terrain is enabled Bug #4410: [Mod] Arktwend: OpenMW does not use default marker definitions Bug #4412: openmw-iniimporter ignores data paths from config Bug #4413: Moving with 0 strength uses all of your fatigue Bug #4420: Camera flickering when I open up and close menus while sneaking Bug #4424: [macOS] Cursor is either empty or garbage when compiled against macOS 10.13 SDK Bug #4435: Item health is considered a signed integer Bug #4441: Adding items to currently disabled weapon-wielding creatures crashes the game Feature #1786: Round up encumbrance value in the encumbrance bar Feature #2694: Editor: rename "model" column to make its purpose clear Feature #3870: Editor: Terrain Texture Brush Button Feature #3872: Editor: Edit functions in terrain texture editing mode Feature #4054: Launcher: Create menu for settings.cfg options Feature #4064: Option for fast travel services to charge for the first companion Feature #4142: Implement fWereWolfHealth GMST Feature #4174: Multiple quicksaves Feature #4407: Support NiLookAtController Feature #4423: Rebalance soul gem values Task #4015: Use AppVeyor build artifact features to make continuous builds available Editor: New (and more complete) icon set 0.43.0 ------ Bug #815: Different settings cause inconsistent underwater visibility Bug #1452: autosave is not executed when waiting Bug #1555: Closing containers with spacebar doesn't work after touching an item Bug #1692: Can't close container when item is "held" Bug #2405: Maximum distance for guards attacking hostile creatures is incorrect Bug #2445: Spellcasting can be interrupted Bug #2489: Keeping map open not persisted between saves Bug #2594: 1st person view uses wrong body texture with Better bodies Bug #2628: enablestatreviewmenu command doen't read race, class and sign values from current game Bug #2639: Attacking flag isn't reset upon reloading Bug #2698: Snow and rain VFX move with the player Bug #2704: Some creature swim animations not being used Bug #2789: Potential risk of misunderstanding using the colored "owned" crosshair feature Bug #3045: Settings containing '#' cannot be loaded Bug #3097: Drop() doesn't work when an item is held (with the mouse) Bug #3110: GetDetected doesn't work without a reference Bug #3126: Framerate nosedives when adjusting dialogue window size Bug #3243: Ampersand in configuration files isn't escaped automatically Bug #3365: Wrong water reflection along banks Bug #3441: Golden saint always dispelling soul trap / spell priority issue Bug #3528: Disposing of corpses breaks quests Bug #3531: No FPS limit when playing bink videos even though "framerate limit" is set in settings.cfg Bug #3647: Multi-effect spells play audio louder than in Vanilla Bug #3656: NPCs forget where their place in the world is Bug #3665: Music transitions are too abrupt Bug #3679: Spell cast effect should disappear after using rest command Bug #3684: Merchants do not restock empty soul gems if they acquire filled ones. Bug #3694: Wrong magicka bonus applied on character creation Bug #3706: Guards don't try to arrest the player if attacked Bug #3709: Editor: Camera is not positioned correctly on mode switches related to orbital mode Bug #3720: Death counter not cleaned of non-existing IDs when loading a game Bug #3744: "Greater/lesser or equal" operators are not parsed when their signs are swapped Bug #3749: Yagrum Bagarn moves to different position on encountering Bug #3766: DisableLevitation does not remove visuals of preexisting effect Bug #3787: Script commands in result box for voiced dialogue are ignored Bug #3793: OpenMW tries to animate animated references even when they are disabled Bug #3794: Default sound buffer size is too small for mods Bug #3796: Mod 'Undress for me' doesn't work: NPCs re-equip everything Bug #3798: tgm command behaviour differs from vanilla Bug #3804: [Mod] Animated Morrowind: some animations do not loop correctly Bug #3805: Slight enchant miscalculation Bug #3826: Rendering problems with an image in a letter Bug #3833: [Mod] Windows Glow: windows textures are much darker than in original game Bug #3835: Bodyparts with multiple NiTriShapes are not handled correctly Bug #3839: InventoryStore::purgeEffect() removes only first effect with argument ID Bug #3843: Wrong jumping fatigue loss calculations Bug #3850: Boethiah's voice is distorted underwater Bug #3851: NPCs and player say things while underwater Bug #3864: Crash when exiting to Khartag point from Ilunibi Bug #3878: Swapping soul gems while enchanting allows constant effect enchantments using any soul gem Bug #3879: Dialogue option: Go to jail, persists beyond quickload Bug #3891: Journal displays empty entries Bug #3892: Empty space before dialogue entry display Bug #3898: (mod) PositionCell in dialogue results closes dialogue window Bug #3906: "Could not find Data Files location" dialog can appear multiple times Bug #3908: [Wizard] User gets stuck if they cancel out of installing from a CD Bug #3909: Morrowind Content Language dropdown is the only element on the right half of the Settings window Bug #3910: Launcher window can be resized so that it cuts off the scroll Bug #3915: NC text key on nifs doesn't work Bug #3919: Closing inventory while cursor hovers over spell (or other magic menu item) produces left click sound Bug #3922: Combat AI should avoid enemy hits when casts Self-ranged spells Bug #3934: [macOS] Copy/Paste from system clipboard uses Control key instead of Command key Bug #3935: Incorrect attack strength for AI actors Bug #3937: Combat AI: enchanted weapons have too high rating Bug #3942: UI sounds are distorted underwater Bug #3943: CPU/GPU usage should stop when the game is minimised Bug #3944: Attempting to sell stolen items back to their owner does not remove them from your inventory Bug #3955: Player's avatar rendering issues Bug #3956: EditEffectDialog: Cancel button does not update a Range button and an Area slider properly Bug #3957: Weird bodypart rendering if a node has reserved name Bug #3960: Clothes with high cost (> 32768) are not handled properly Bug #3963: When on edge of being burdened the condition doesn't lower as you run. Bug #3971: Editor: Incorrect colour field in cell table Bug #3974: Journal page turning doesn't produce sounds Bug #3978: Instant opening and closing happens when using a Controller with Menus/Containers Bug #3981: Lagging when spells are cast, especially noticeable on new landmasses such as Tamriel Rebuilt Bug #3982: Down sounds instead of Up ones are played when trading Bug #3987: NPCs attack after some taunting with no "Goodbye" Bug #3991: Journal can still be opened at main menu Bug #3995: Dispel cancels every temporary magic effect Bug #4002: Build broken on OpenBSD with clang Bug #4003: Reduce Render Area of Inventory Doll to Fit Within Border Bug #4004: Manis Virmaulese attacks without saying anything Bug #4010: AiWander: "return to the spawn position" feature does not work properly Bug #4016: Closing menus with spacebar will still send certain assigned actions through afterwards Bug #4017: GetPCRunning and GetPCSneaking should check that the PC is actually moving Bug #4024: Poor music track distribution Bug #4025: Custom spell with copy-pasted name always sorts to top of spell list Bug #4027: Editor: OpenMW-CS misreports its own name as "OpenCS", under Mac OS Bug #4033: Archers don't attack if the arrows have run out and there is no other weapon Bug #4037: Editor: New greetings do not work in-game. Bug #4049: Reloading a saved game while falling prevents damage Bug #4056: Draw animation should not be played when player equips a new weapon Bug #4074: Editor: Merging of LAND/LTEX records Bug #4076: Disposition bar is not updated when "goodbye" selected in dialogue Bug #4079: Alchemy skill increases do not take effect until next batch Bug #4093: GetResistFire, getResistFrost and getResistShock doesn't work as in vanilla Bug #4094: Level-up messages for levels past 20 are hardcoded not to be used Bug #4095: Error in framelistener when take all items from a dead corpse Bug #4096: Messagebox with the "%0.f" format should use 0 digit precision Bug #4104: Cycling through weapons does not skip broken ones Bug #4105: birthsign generation menu does not show full details Bug #4107: Editor: Left pane in Preferences window is too narrow Bug #4112: Inventory sort order is inconsistent Bug #4113: 'Resolution not supported in fullscreen' message is inconvenient Bug #4131: Pickpocketing behaviour is different from vanilla Bug #4155: NPCs don't equip a second ring in some cases Bug #4156: Snow doesn't create water ripples Bug #4165: NPCs autoequip new clothing with the same price Feature #452: Rain-induced water ripples Feature #824: Fading for doors and teleport commands Feature #933: Editor: LTEX record table Feature #936: Editor: LAND record table Feature #1374: AI: Resurface to breathe Feature #2320: ess-Importer: convert projectiles Feature #2509: Editor: highlighting occurrences of a word in a script Feature #2748: Editor: Should use one resource manager per document Feature #2834: Have openMW's UI remember what menu items were 'pinned' across boots. Feature #2923: Option to show the damage of the arrows through tooltip. Feature #3099: Disabling inventory while dragging an item forces you to drop it Feature #3274: Editor: Script Editor - Shortcuts and context menu options for commenting code out and uncommenting code respectively Feature #3275: Editor: User Settings- Add an option to reset settings to their default status (per category / all) Feature #3400: Add keyboard shortcuts for menus Feature #3492: Show success rate while enchanting Feature #3530: Editor: Reload data files Feature #3682: Editor: Default key binding reset Feature #3921: Combat AI: aggro priorities Feature #3941: Allow starting at an unnamed exterior cell with --start Feature #3952: Add Visual Studio 2017 support Feature #3953: Combat AI: use "WhenUsed" enchantments Feature #4082: Leave the stack of ingredients or potions grabbed after using an ingredient/potion Task #2258: Windows installer: launch OpenMW tickbox Task #4152: The Windows CI script is moving files around that CMake should be dealing with 0.42.0 ------ Bug #1956: Duplicate objects after loading the game, when a mod was edited Bug #2100: Falling leaves in Vurt's Leafy West Gash II not rendered correctly Bug #2116: Cant fit through some doorways pressed against staircases Bug #2289: Some modal dialogs are not centered on the screen when the window resizes Bug #2409: Softlock when pressing weapon/magic switch keys during chargen, afterwards switches weapons even though a text field is selected Bug #2483: Previous/Next Weapon hotkeys triggered while typing the name of game save Bug #2629: centeroncell, coc causes death / fall damage time to time when teleporting from high Bug #2645: Cycling weapons is possible while console/pause menu is open Bug #2678: Combat with water creatures do not end upon exiting water Bug #2759: Light Problems in Therana's Chamber in Tel Branora Bug #2771: unhandled sdl event of type 0x302 Bug #2777: (constant/on cast) disintegrate armor/weapon on self is seemingly not working Bug #2838: Editor: '.' in a record name should be allowed Bug #2909: NPCs appear floating when standing on a slope Bug #3093: Controller movement cannot be used while mouse is moving Bug #3134: Crash possible when using console with open container Bug #3254: AI enemies hit between them. Bug #3344: Editor: Verification results sorting by Type is not alphabetical. Bug #3345: Editor: Cloned and added pathgrids are lost after reopen of saved omwgame file Bug #3355: [MGSO] Physics maxing out in south cornerclub Balmora Bug #3484: Editor: camera position is not set when changing cell via drag&drop Bug #3508: Slowfall kills Jump momentum Bug #3580: Crash: Error ElementBufferObject::remove BufferData<0> out of range Bug #3581: NPCs wander too much Bug #3601: Menu Titles not centered vertically Bug #3607: [Mac OS] Beginning of NPC speech cut off (same issue as closed bug #3453) Bug #3613: Can not map "next weapon" or "next spell" to controller Bug #3617: Enchanted arrows don't explode when hitting the ground Bug #3645: Unable to use steps in Vivec, Palace of Vivec Bug #3650: Tamriel Rebuilt 16.09.1 – Hist Cuirass GND nif is rendered inside a Pink Box Bug #3652: Item icon shadows get stuck in the alchemy GUI Bug #3653: Incorrect swish sounds Bug #3666: NPC collision should not be disabled until death animation has finished Bug #3669: Editor: Text field was missing from book object editing dialogue Bug #3670: Unhandled SDL event of type 0x304 Bug #3671: Incorrect local variable value after picking up bittercup Bug #3686: Travelling followers doesn't increase travel fee Bug #3689: Problematic greetings from Antares Big Mod that override the appropriate ones. Bug #3690: Certain summoned creatures do not engage in combat with underwater creatures Bug #3691: Enemies do not initiate combat with player followers on sight Bug #3695: [Regression] Dispel does not always dispel spell effects in 0.41 Bug #3699: Crash on MWWorld::ProjectileManager::moveMagicBolts Bug #3700: Climbing on rocks and mountains Bug #3704: Creatures don't auto-equip their shields on creation Bug #3705: AI combat engagement logic differs from vanilla Bug #3707: Animation playing does some very odd things if pc comes in contact with the animated mesh Bug #3712: [Mod] Freeze upon entering Adanumuran with mod Adanumuran Reclaimed Bug #3713: [Regression] Cancelling dialogue or using travel with creatures throws a (possibly game-breaking) exception Bug #3719: Dropped identification papers can't be picked up again Bug #3722: Command spell doesn't bring enemies out of combat Bug #3727: Using "Activate" mid-script-execution invalidates interpreter context Bug #3746: Editor: Book records show attribute IDs instead of skill IDs for teached skills entry. Bug #3755: Followers stop following after loading from savegame Bug #3772: ModStat lowers attribute to 100 if it was greater Bug #3781: Guns in Clean Hunter Rifles mod use crossbow sounds Bug #3797: NPC and creature names don't show up in combat when RMB windows are displayed Bug #3800: Wrong tooltip maximum width Bug #3801: Drowning widget is bugged Bug #3802: BarterOffer shouldn't limit pcMercantile Bug #3813: Some fatal error Bug #3816: Expression parser thinks the -> token is unexpected when a given explicit refID clashes with a journal ID Bug #3822: Custom added creatures are not animated Feature #451: Water sounds Feature #2691: Light particles sometimes not shown in inventory character preview Feature #3523: Light source on magic projectiles Feature #3644: Nif NiSphericalCollider Unknown Record Type Feature #3675: ess-Importer: convert mark location Feature #3693: ess-Importer: convert last known exterior cell Feature #3748: Editor: Replace "Scroll" check box in Book records with "Book Type" combo box. Feature #3751: Editor: Replace "Xyz Blood" check boxes in NPC and Creature records with "Blood Type" combo box Feature #3752: Editor: Replace emitter check boxes in Light records with "Emitter Type" combo box Feature #3756: Editor: Replace "Female" check box in NPC records with "Gender" combo box Feature #3757: Editor: Replace "Female" check box in BodyPart records with "Gender" combo box Task #3092: const version of ContainerStoreIterator Task #3795: /deps folder not in .gitignore 0.41.0 ------ Bug #1138: Casting water walking doesn't move the player out of the water Bug #1931: Rocks from blocked passage in Bamz-Amschend, Radacs Forge can reset and cant be removed again. Bug #2048: Almvisi and Divine Intervention display wrong spell effect Bug #2054: Show effect-indicator for "instant effect" spells and potions Bug #2150: Clockwork City door animation problem Bug #2288: Playback of weapon idle animation not correct Bug #2410: Stat-review window doesn't display starting spells, powers, or abilities Bug #2493: Repairing occasionally very slow Bug #2716: [OSG] Water surface is too transparent from some angles Bug #2859: [MAC OS X] Cannot exit fullscreen once enabled Bug #3091: Editor: will not save addon if global variable value type is null Bug #3277: Editor: Non-functional nested tables in subviews need to be hidden instead of being disabled Bug #3348: Disabled map markers show on minimap Bug #3350: Extending selection to instances with same object results in duplicates. Bug #3353: [Mod] Romance version 3.7 script failed Bug #3376: [Mod] Vampire Embrace script fails to execute Bug #3385: Banners don't animate in stormy weather as they do in the original game Bug #3393: Akulakhan re-enabled after main quest Bug #3427: Editor: OpenMW-CS instances won´t get deleted Bug #3451: Feril Salmyn corpse isn't where it is supposed to be Bug #3497: Zero-weight armor is displayed as "heavy" in inventory tooltip Bug #3499: Idle animations don't always loop Bug #3500: Spark showers at Sotha Sil do not appear until you look at the ceiling Bug #3515: Editor: Moved objects in interior cells are teleported to exterior cells. Bug #3520: Editor: OpenMW-CS cannot find project file when launching the game Bug #3521: Armed NPCs don't use correct melee attacks Bug #3535: Changing cell immediately after dying causes character to freeze. Bug #3542: Unable to rest if unalerted slaughterfish are in the cell with you Bug #3549: Blood effects occur even when a hit is resisted Bug #3551: NPC Todwendy in german version can't interact Bug #3552: Opening the journal when fonts are missing results in a crash Bug #3555: SetInvisible command should not apply graphic effect Bug #3561: Editor: changes from omwaddon are not loaded in [New Addon] mode Bug #3562: Non-hostile NPCs can be disarmed by stealing their weapons via sneaking Bug #3564: Editor: openmw-cs verification results Bug #3568: Items that should be invisible are shown in the inventory Bug #3574: Alchemy: Alembics and retorts are used in reverse Bug #3575: Diaglog choices don't work in mw 0.40 Bug #3576: Minor differences in AI reaction to hostile spell effects Bug #3577: not local nolore dialog test Bug #3578: Animation Replacer hangs after one cicle/step Bug #3579: Bound Armor skillups and sounds Bug #3583: Targetted GetCurrentAiPackage returns 0 Bug #3584: Persuasion bug Bug #3590: Vendor, Ilen Faveran, auto equips items from stock Bug #3594: Weather doesn't seem to update correctly in Mournhold Bug #3598: Saving doesn't save status of objects Bug #3600: Screen goes black when trying to travel to Sadrith Mora Bug #3608: Water ripples aren't created when walking on water Bug #3626: Argonian NPCs swim like khajiits Bug #3627: Cannot delete "Blessed touch" spell from spellbook Bug #3634: An enchanted throwing weapon consumes charges from the stack in your inventory. (0.40.0) Bug #3635: Levelled items in merchants are "re-rolled" (not bug 2952, see inside) Feature #1118: AI combat: flee Feature #1596: Editor: Render water Feature #2042: Adding a non-portable Light to the inventory should cause the player to glow Feature #3166: Editor: Instance editing mode - rotate sub mode Feature #3167: Editor: Instance editing mode - scale sub mode Feature #3420: ess-Importer: player control flags Feature #3489: You shouldn't be be able to re-cast a bound equipment spell Feature #3496: Zero-weight boots should play light boot footsteps Feature #3516: Water Walking should give a "can't cast" message and fail when you are too deep Feature #3519: Play audio and visual effects for all effects in a spell Feature #3527: Double spell explosion scaling Feature #3534: Play particle textures for spell effects Feature #3539: Make NPCs use opponent's weapon range to decide whether to dodge Feature #3540: Allow dodging for creatures with "biped" flag Feature #3545: Drop shadow for items in menu Feature #3558: Implement same spell range for "on touch" spells as original engine Feature #3560: Allow using telekinesis with touch spells on objects Task #3585: Some objects added by Morrowind Rebirth do not display properly their texture 0.40.0 ------ Bug #1320: AiWander - Creatures in cells without pathgrids do not wander Bug #1873: Death events are triggered at the beginning of the death animation Bug #1996: Resting interrupts magic effects Bug #2399: Vampires can rest in broad daylight and survive the experience Bug #2604: Incorrect magicka recalculation Bug #2721: Telekinesis extends interaction range where it shouldn't Bug #2981: When waiting, NPCs can go where they wouldn't go normally. Bug #3045: Esp files containing the letter '#' in the file name cannot be loaded on startup Bug #3071: Slowfall does not stop momentum when jumping Bug #3085: Plugins can not replace parent cell references with a cell reference of different type Bug #3145: Bug with AI Cliff Racer. He will not attack you, unless you put in front of him. Bug #3149: Editor: Weather tables were missing from regions Bug #3201: Netch shoots over your head Bug #3269: If you deselect a mod and try to load a save made inside a cell added by it, you end bellow the terrain in the grid 0/0 Bug #3286: Editor: Script editor tab width Bug #3329: Teleportation spells cause crash to desktop after build update from 0.37 to 0.38.0 Bug #3331: Editor: Start Scripts table: Adding a script doesn't refresh the list of Start Scripts and allows to add a single script multiple times Bug #3332: Editor: Scene view: Tool tips only occur when holding the left mouse button Bug #3340: ESS-Importer does not separate item stacks Bug #3342: Editor: Creation of pathgrids did not check if the pathgrid already existed Bug #3346: "Talked to PC" is always 0 for "Hello" dialogue Bug #3349: AITravel doesn't repeat Bug #3370: NPCs wandering to invalid locations after training Bug #3378: "StopCombat" command does not function in vanilla quest Bug #3384: Battle at Nchurdamz - Larienna Macrina does not stop combat after killing Hrelvesuu Bug #3388: Monster Respawn tied to Quicksave Bug #3390: Strange visual effect in Dagoth Ur's chamber Bug #3391: Inappropriate Blight weather behavior at end of main quest Bug #3394: Replaced dialogue inherits some of its old data Bug #3397: Actors that start the game dead always have the same death pose Bug #3401: Sirollus Saccus sells not glass arrows Bug #3402: Editor: Weapon data not being properly set Bug #3405: Mulvisic Othril will not use her chitin throwing stars Bug #3407: Tanisie Verethi will immediately detect the player Bug #3408: Improper behavior of ashmire particles Bug #3412: Ai Wander start time resets when saving/loading the game Bug #3416: 1st person and 3rd person camera isn't converted from .ess correctly Bug #3421: Idling long enough while paralyzed sometimes causes character to get stuck Bug #3423: Sleep interruption inside dungeons too agressive Bug #3424: Pickpocketing sometimes won't work Bug #3432: AiFollow / AiEscort durations handled incorrectly Bug #3434: Dead NPC's and Creatures still contribute to sneak skill increases Bug #3437: Weather-conditioned dialogue should not play in interiors Bug #3439: Effects cast by summon stick around after their death Bug #3440: Parallax maps looks weird Bug #3443: Class graphic for custom class should be Acrobat Bug #3446: OpenMW segfaults when using Atrayonis's "Anthology Solstheim: Tomb of the Snow Prince" mod Bug #3448: After dispelled, invisibility icon is still displayed Bug #3453: First couple of seconds of NPC speech is muted Bug #3455: Portable house mods lock player and npc movement up exiting house. Bug #3456: Equipping an item will undo dispel of constant effect invisibility Bug #3458: Constant effect restore health doesn't work during Wait Bug #3466: It is possible to stack multiple scroll effects of the same type Bug #3471: When two mods delete the same references, many references are not disabled by the engine. Bug #3473: 3rd person camera can be glitched Feature #1424: NPC "Face" function Feature #2974: Editor: Multiple Deletion of Subrecords Feature #3044: Editor: Render path grid v2 Feature #3362: Editor: Configurable key bindings Feature #3375: Make sun / moon reflections weather dependent Feature #3386: Editor: Edit pathgrid 0.39.0 ------ Bug #1384: Dark Brotherhood Assassin (and other scripted NPCs?) spawns beneath/inside solid objects Bug #1544: "Drop" drops equipped item in a separate stack Bug #1587: Collision detection glitches Bug #1629: Container UI locks up in Vivec at Jeanne's Bug #1771: Dark Brotherhood Assassin oddity in Eight Plates Bug #1827: Unhandled NiTextureEffect in ex_dwrv_ruin30.nif Bug #2089: When saving while swimming in water in an interior cell, you will be spawned under water on loading Bug #2295: Internal texture not showing, nipixeldata Bug #2363: Corpses don't disappear Bug #2369: Respawns should be timed individually Bug #2393: Сharacter is stuck in the tree Bug #2444: [Mod] NPCs from Animated Morrowind appears not using proper animations Bug #2467: Creatures do not respawn Bug #2515: Ghosts in Ibar-Dad spawn stuck in walls Bug #2610: FixMe script still needs to be implemented Bug #2689: Riekling raider pig constantly screams while running Bug #2719: Vivec don't put their hands on the knees with this replacer (Psymoniser Vivec God Replacement NPC Edition v1.0 Bug #2737: Camera shaking when side stepping around object Bug #2760: AI Combat Priority Problem - Use of restoration spell instead of attacking Bug #2806: Stack overflow in LocalScripts::getNext Bug #2807: Collision detection allows player to become stuck inside objects Bug #2814: Stairs to Marandus have improper collision Bug #2925: Ranes Ienith will not appear, breaking the Morag Tong and Thieves Guid questlines Bug #3024: Editor: Creator bar in startscript subview does not accept script ID drops Bug #3046: Sleep creature: Velk is spawned half-underground in the Thirr River Valley Bug #3080: Calling aifollow without operant in local script every frame causes mechanics to overheat + log Bug #3101: Regression: White guar does not move Bug #3108: Game Freeze after Killing Diseased Rat in Foreign Quarter Tomb Bug #3124: Bloodmoon Quest - Rite of the Wolf Giver (BM_WolfGiver) – Innocent victim won't turn werewolf Bug #3125: Improper dialogue window behavior when talking to creatures Bug #3130: Some wandering NPCs disappearing, cannot finish quests Bug #3132: Editor: GMST ID named sMake Enchantment is instead named sMake when making new game from scratch Bug #3133: OpenMW and the OpenCS are writting warnings about scripts that use the function GetDisabled. Bug #3135: Journal entry for The Pigrim's Path missing name Bug #3136: Dropped bow is displaced Bug #3140: Editor: OpenMW-CS fails to open newly converted and saved omwaddon file. Bug #3142: Duplicate Resist Magic message Bug #3143: Azura missing her head Bug #3146: Potion effect showing when ingredient effects are not known Bug #3155: When executing chop attack with a spear, hands turn partly invisible Bug #3161: Fast travel from Silt Strider or Boat Ride will break save files made afterwards Bug #3163: Editor: Objects dropped to scene do not always save Bug #3173: Game Crashes After Casting Recall Spell Bug #3174: Constant effect enchantments play spell animation on dead bodies Bug #3175: Spell effects do not wear down when caster dies Bug #3176: NPCs appearing randomly far away from towns Bug #3177: Submerged corpse floats ontop of water when it shouldn't (Widow Vabdas' Deed quest) Bug #3184: Bacola Closcius in Balmora, South Wall Cornerclub spams magic effects if attacked Bug #3207: Editor: New objects do not render Bug #3212: Arrow of Ranged Silence Bug #3213: Looking at Floor After Magical Transport Bug #3220: The number of remaining ingredients in the alchemy window doesn't go down when failing to brew a potion Bug #3222: Falling through the water in Vivec Bug #3223: Crash at the beginning with MOD (The Symphony) Bug #3228: Purple screen when leveling up. Bug #3233: Infinite disposition via MWDialogue::Filter::testDisposition() glitch Bug #3234: Armor mesh stuck on body in inventory menu Bug #3235: Unlike vanilla, OpenMW don't allow statics and activators cast effects on the player. Bug #3238: Not loading cells when using Poorly Placed Object Fix.esm Bug #3248: Editor: Using the "Next Script" and "Previous Script" buttons changes the record status to "Modified" Bug #3258: Woman biped skeleton Bug #3259: No alternating punches Bug #3262: Crash in class selection menu Bug #3279: Load menu: Deleting a savegame makes scroll bar jump to the top Bug #3326: Starting a new game, getting to class selection, then starting another new game temporarily assigns Acrobat class Bug #3327: Stuck in table after loading when character was sneaking when quicksave Feature #652: Editor: GMST verifier Feature #929: Editor: Info record verifier Feature #1279: Editor: Render cell border markers Feature #2482: Background cell loading and caching of loaded cells Feature #2484: Editor: point lighting Feature #2801: Support NIF bump map textures in osg Feature #2926: Editor: Optional line wrap in script editor wrap lines Feature #3000: Editor: Reimplement 3D scene camera system Feature #3035: Editor: Make scenes a drop target for referenceables Feature #3043: Editor: Render cell markers v2 Feature #3164: Editor: Instance Selection Menu Feature #3165: Editor: Instance editing mode - move sub mode Feature #3244: Allow changing water Level of Interiors behaving like exteriors Feature #3250: Editor: Use "Enter" key instead of clicking "Create" button to confirm ID input in Creator Bar Support #3179: Fatal error on startup 0.38.0 ------ Bug #1699: Guard will continuously run into mudcrab Bug #1934: Saw in Dome of Kasia doesnt harm the player Bug #1962: Rat floats when killed near the door Bug #1963: Kwama eggsacks pulse too fast Bug #2198: NPC voice sound source should be placed at their head Bug #2210: OpenMW installation wizard crashes... Bug #2211: Editor: handle DELE subrecord at the end of a record Bug #2413: ESM error Unknown subrecord in Grandmaster of Hlaalu Bug #2537: Bloodmoon quest Ristaag: Sattir not consistently dying, plot fails to advance; same with Grerid Bug #2697: "The Swimmer" moves away after leading you to underwater cave Bug #2724: Loading previous save duplicates containers and harvestables Bug #2769: Inventory doll - Cursor not respecting order of clothes Bug #2865: Scripts silently fail when moving NPCs between cells. Bug #2873: Starting a new game leads to CTD / Fatal Error Bug #2918: Editor: it's not possible to create an omwaddon containing a dot in the file name Bug #2933: Dialog box can't disable a npc if it is in another cell. (Rescue Madura Seran). Bug #2942: atronach sign behavior (spell absorption) changes when trying to receive a blessing at "shrine of tribunal" Bug #2952: Enchantment Merchant Items reshuffled EVERY time 'barter' is clicked Bug #2961: ESM Error: Unknown subrecord if Deus Ex Machina mod is loaded Bug #2972: Resurrecting the player via console does not work when health was 0 Bug #2986: Projectile weapons work underwater Bug #2988: "Expected subrecord" bugs showing up. Bug #2991: Can't use keywords in strings for MessageBox Bug #2993: Tribunal:The Shrine of the Dead – Urvel Dulni can't stop to follow the player. Bug #3008: NIFFile Error while loading meshes with a NiLODNode Bug #3010: Engine: items should sink to the ground when dropped under water Bug #3011: NIFFile Error while loading meshes with a NiPointLight Bug #3016: Engine: something wrong with scripting - crash / fatal error Bug #3020: Editor: verify does not check if given "item ID" (as content) for a "container" exists Bug #3026: [MOD: Julan Ashlander Companion] Dialogue not triggering correctly Bug #3028: Tooltips for Health, Magicka and Fatigue show in Options menu even when bars aren't visible Bug #3034: Item count check dialogue option doesn't work (Guards accept gold even if you don't have enough) Bug #3036: Owned tooltip color affects spell tooltips incorrrectly Bug #3037: Fatal error loading old ES_Landscape.esp in Store::search Bug #3038: Player sounds come from underneath Bug #3040: Execution of script failed: There is a message box already Bug #3047: [MOD: Julan Ashlander Companion] Scripts KS_Bedscript or KS_JulanNight not working as intended Bug #3048: Fatal Error Bug #3051: High field of view results in first person rendering glitches Bug #3053: Crash on new game at character class selection Bug #3058: Physiched sleeves aren't rendered correctly. Bug #3060: NPCs use wrong landing sound Bug #3062: Mod support regression: Andromeda's fast travel. Bug #3063: Missing Journal Textures without Tribunal and Bloodmoon installed Bug #3077: repeated aifollow causes the distance to stack Bug #3078: Creature Dialogues not showing when certain Function/Conditions are required. Bug #3082: Crash when entering Holamayan Monastery with mesh replacer installed Bug #3086: Party at Boro's House – Creature with Class don't talk under OpenMW Bug #3089: Dreamers spawn too soon Bug #3100: Certain controls erroneously work as a werewolf Bug #3102: Multiple unique soultrap spell sources clone souls. Bug #3105: Summoned creatures and objects disappear at midnight Bug #3112: gamecontrollerdb file creation with wrong extension Bug #3116: Dialogue Function "Same Race" is avoided Bug #3117: Dialogue Bug: Choice conditions are tested when not in a choice Bug #3118: Body Parts are not rendered when used in a pose. Bug #3122: NPC direction is reversed during sneak awareness check Feature #776: Sound effects from one direction don't necessarily affect both speakers in stereo Feature #858: Different fov settings for hands and the game world Feature #1176: Handle movement of objects between cells Feature #2507: Editor: choosing colors for syntax highlighting Feature #2867: Editor: hide script error list when there are no errors Feature #2885: Accept a file format other than nif Feature #2982: player->SetDelete 1 results in: PC can't move, menu can be opened Feature #2996: Editor: make it possible to preset the height of the script check area in a script view Feature #3014: Editor: Tooltips in 3D scene Feature #3064: Werewolf field of view Feature #3074: Quicksave indicator Task #287: const version of Ptr Task #2542: Editor: redo user settings system 0.37.0 ------ Bug #385: Light emitting objects have a too short distance of activation Bug #455: Animation doesn't resize creature's bounding box Bug #602: Only collision model is updated when modifying objects trough console Bug #639: Sky horizon at nighttime Bug #672: incorrect trajectory of the moons Bug #814: incorrect NPC width Bug #827: Inaccurate raycasting for dead actors Bug #996: Can see underwater clearly when at right height/angle Bug #1317: Erene Llenim in Seyda Neen does not walk around Bug #1330: Cliff racers fail to hit the player Bug #1366: Combat AI can't aim down (in order to hit small creatures) Bug #1511: View distance while under water is much too short Bug #1563: Terrain positioned incorrectly and appears to vibrate in far-out cells Bug #1612: First person models clip through walls Bug #1647: Crash switching from full screen to windows mode - D3D9 Bug #1650: No textures with directx on windows Bug #1730: Scripts names starting with digit(s) fail to compile Bug #1738: Socucius Ergalla's greetings are doubled during the tutorial Bug #1784: First person weapons always in the same position Bug #1813: Underwater flora lighting up entire area. Bug #1871: Handle controller extrapolation flags Bug #1921: Footstep frequency and velocity do not immediately update when speed attribute changes Bug #2001: OpenMW crashes on start with OpenGL 1.4 drivers Bug #2014: Antialiasing setting does nothing on Linux Bug #2037: Some enemies attack the air when spotting the player Bug #2052: NIF rotation matrices including scales are not supported Bug #2062: Crank in Old Mournhold: Forgotten Sewer turns about the wrong axis Bug #2111: Raindrops in front of fire look wrong Bug #2140: [OpenGL] Water effects, flames and parts of creatures solid black when observed through brazier flame Bug #2147: Trueflame and Hopesfire flame effects not properly aligned with blade Bug #2148: Verminous fabricants have little coloured box beneath their feet Bug #2149: Sparks in Clockwork City should bounce off the floor Bug #2151: Clockwork City dicer trap doesn't activate when you're too close Bug #2186: Mini map contains scrambled pixels that cause the mini map to flicker Bug #2187: NIF file with more than 255 NiBillboardNodes does not load Bug #2191: Editor: Crash when trying to view cell in render view in OpenCS Bug #2270: Objects flicker transparently Bug #2280: Latest 32bit windows build of openmw runns out of vram Bug #2281: NPCs don't scream when they die Bug #2286: Jumping animation restarts when equipping mid-air Bug #2287: Weapon idle animation stops when turning Bug #2355: Light spell doesn't work in 1st person view Bug #2362: Lantern glas opaque to flame effect from certain viewing angles Bug #2364: Light spells are not as bright as in Morrowind Bug #2383: Remove the alpha testing override list Bug #2436: Crash on entering cell "Tower of Tel Fyr, Hall of Fyr" Bug #2457: Player followers should not report crimes Bug #2458: crash in some fighting situations Bug #2464: Hiding an emitter node should make that emitter stop firing particles Bug #2466: Can't load a save created with OpenMW-0.35.0-win64 Bug #2468: music from title screen continues after loading savegame Bug #2494: Map not consistent between saves Bug #2504: Dialog scroll should always start at the top Bug #2506: Editor: Undo/Redo shortcuts do not work in script editor Bug #2513: Mannequins in mods appear as dead bodies Bug #2524: Editor: TopicInfo "custom" condition section is missing Bug #2540: Editor: search and verification result table can not be sorted by clicking on the column names Bug #2543: Editor: there is a problem with spell effects Bug #2544: Editor fails to save NPC information correctly. Bug #2545: Editor: delete record in Objects (referenceables) table messes up data Bug #2546: Editor: race base attributes and skill boni are not displayed, thus not editable Bug #2547: Editor: some NPC data is not displayed, thus not editable Bug #2551: Editor: missing data in cell definition Bug #2553: Editor: value filter does not work for float values Bug #2555: Editor: undo leaves the record status as Modified Bug #2559: Make Detect Enchantment marks appear on top of the player arrow Bug #2563: position consoling npc doesn't work without cell reload Bug #2564: Editor: Closing a subview from code does not clean up properly and will lead to crash on opening the next subview Bug #2568: Editor: Setting default window size is ignored Bug #2569: Editor: saving from an esp to omwaddon file results in data loss for TopicInfo Bug #2575: Editor: Deleted record (with Added (ModifiedOnly) status) remains in the Dialog SubView Bug #2576: Editor: Editor doesn't scroll to a newly opened subview, when ScrollBar Only mode is active Bug #2578: Editor: changing Level or Reputation of an NPC crashes the editor Bug #2579: Editor: filters not updated when adding or cloning records Bug #2580: Editor: omwaddon makes OpenMW crash Bug #2581: Editor: focus problems in edit subviews single- and multiline input fields Bug #2582: Editor: object verifier should check for non-existing scripts being referenced Bug #2583: Editor: applying filter to TopicInfo on mods that have added dialouge makes the Editor crash Bug #2586: Editor: some dialogue only editable items do not refresh after undo Bug #2588: Editor: Cancel button exits program Bug #2589: Editor: Regions table - mapcolor does not change correctly Bug #2591: Placeatme - spurious 5th parameter raises error Bug #2593: COC command prints multiple times when GUI is hidden Bug #2598: Editor: scene view of instances has to be zoomed out to displaying something - center camera instance please Bug #2607: water behind an invisible NPC becomes invisible as well Bug #2611: Editor: Sort problem in Objects table when few nested rows are added Bug #2621: crash when a creature has no model Bug #2624: Editor: missing columns in tables Bug #2627: Character sheet doesn't properly update when backing out of CharGen Bug #2642: Editor: endif without if - is not reported as error when "verify" was executed Bug #2644: Editor: rebuild the list of available content files when opening the open/new dialogues Bug #2656: OpenMW & OpenMW-CS: setting "Flies" flag for ghosts has no effect Bug #2659: OpenMW & OpenMW-CS: savegame load fail due to script attached to NPCs Bug #2668: Editor: reputation value in the input field is not stored Bug #2696: Horkers use land idle animations under water Bug #2705: Editor: Sort by Record Type (Objects table) is incorrect Bug #2711: Map notes on an exterior cell that shows up with a map marker on the world map do not show up in the tooltip for that cell's marker on the world map Bug #2714: Editor: Can't reorder rows with the same topic in different letter case Bug #2720: Head tracking for creatures not implemented Bug #2722: Alchemy should only include effects shared by at least 2 ingredients Bug #2723: "ori" console command is not working Bug #2726: Ashlanders in front of Ghostgate start wandering around Bug #2727: ESM writer does not handle encoding when saving the TES3 header Bug #2728: Editor: Incorrect position of an added row in Info tables Bug #2731: Editor: Deleting a record triggers a Qt warning Bug #2733: Editor: Undo doesn't restore the Modified status of a record when a nested data is changed Bug #2734: Editor: The Search doesn't work Bug #2738: Additive moon blending Bug #2746: NIF node names should be case insensitive Bug #2752: Fog depth/density not handled correctly Bug #2753: Editor: line edit in dialogue subview tables shows after a single click Bug #2755: Combat AI changes target too frequently Bug #2761: Can't attack during block animations Bug #2764: Player doesn't raise arm in 3rd person for weathertype 9 Bug #2768: Current screen resolution not selected in options when starting OpenMW Bug #2773: Editor: Deleted scripts are editable Bug #2776: ordinators still think I'm wearing their helm even though Khajiit and argonians can't Bug #2779: Slider bars continue to move if you don't release mouse button Bug #2781: sleep interruption is a little off (is this an added feature?) Bug #2782: erroneously able to ready weapon/magic (+sheathe weapon/magic) while paralyzed Bug #2785: Editor: Incorrect GMSTs for newly created omwgame files Bug #2786: Kwama Queen head is inverted under OpenMW Bug #2788: additem and removeitem incorrect gold behavior Bug #2790: --start doesn't trace down Bug #2791: Editor: Listed attributes and skill should not be based on number of NPC objects. Bug #2792: glitched merchantile/infinite free items Bug #2794: Need to ignore quotes in names of script function Bug #2797: Editor: Crash when removing the first row in a nested table Bug #2800: Show an error message when S3TC support is missing Bug #2811: Targetted Open spell effect persists. Bug #2819: Editor: bodypart's race filter not displayed correctly Bug #2820: Editor: table sorting is inverted Bug #2821: Editor: undo/redo command labels are incorrect Bug #2826: locking beds that have been locked via magic psuedo-freezes the game Bug #2830: Script compiler does not accept IDs as instruction/functions arguments if the ID is also a keyword Bug #2832: Cell names are not localized on the world map Bug #2833: [cosmetic] Players swimming at water's surface are slightly too low. Bug #2840: Save/load menu is not entirely localized Bug #2853: [exploit/bug] disintegrate weapon incorrectly applying to lockpicks, probes. creates unbreakable lockpicks Bug #2855: Mouse wheel in journal is not disabled by "Options" panel. Bug #2856: Heart of Lorkhan doesn't visually respond to attacks Bug #2863: Inventory highlights wrong category after load Bug #2864: Illuminated Order 1.0c Bug – The teleport amulet is not placed in the PC inventory. Bug #2866: Editor: use checkbox instead of combobox for boolean values Bug #2875: special cases of fSleepRandMod not behaving properly. Bug #2878: Editor: Verify reports "creature has non-positive level" but there is no level setting Bug #2879: Editor: entered value of field "Buys *" is not saved for a creature Bug #2880: OpenMW & OpenMW-CS: having a scale value of 0.000 makes the game laggy Bug #2882: Freeze when entering cell "Guild of Fighters (Ald'ruhn)" after dropping some items inside Bug #2883: game not playable if mod providing a spell is removed but the list of known spells still contains it Bug #2884: NPC chats about wrong player race Bug #2886: Adding custom races breaks existing numbering of PcRace Bug #2888: Editor: value entered in "AI Wander Idle" is not kept Bug #2889: Editor: creatures made with the CS (not cloned) are always dead Bug #2890: Editor: can't make NPC say a specific "Hello" voice-dialouge Bug #2893: Editor: making a creature use textual dialogue doesn't work. Bug #2901: Editor: gold for trading can not be set for creatures Bug #2907: looking from uderwater part of the PC that is below the surface looks like it would be above the water Bug #2914: Magicka not recalculated on character generation Bug #2915: When paralyzed, you can still enter and exit sneak Bug #2917: chameleon does not work for creatures Bug #2927: Editor: in the automatic script checker local variable caches are not invalidated/updated on modifications of other scripts Bug #2930: Editor: AIWander Idle can not be set for a creature Bug #2932: Editor: you can add rows to "Creature Attack" but you can not enter values Bug #2938: Editor: Can't add a start script. Bug #2944: Spell chance for power to show as 0 on hud when used Bug #2953: Editor: rightclick in an empty place in the menu bar shows an unnamed checkbox Bug #2956: Editor: freezes while editing Filter Bug #2959: space character in field enchantment (of an amulet) prevents rendering of surroundings Bug #2962: OpenMW: Assertion `it != invStore.end()' failed Bug #2964: Recursive script execution can corrupt script runtime data Bug #2973: Editor: placing a chest in the game world and activating it heavily blurrs the character portrait Bug #2978: Editor: Cannot edit alchemy ingredient properties Bug #2980: Editor: Attribute and Skill can be selected for spells that do not require these parameters, leading to non-functional spells Bug #2990: Compiling a script with warning mode 2 and enabled error downgrading leads to infinite recursion Bug #2992: [Mod: Great House Dagoth] Killing Dagoth Gares freezes the game Bug #3007: PlaceItem takes radians instead of degrees + angle reliability Feature #706: Editor: Script Editor enhancements Feature #872: Editor: Colour values in tables Feature #880: Editor: ID auto-complete Feature #928: Editor: Partial sorting in info tables Feature #942: Editor: Dialogue for editing/viewing content file meta information Feature #1057: NiStencilProperty Feature #1278: Editor: Mouse picking in worldspace widget Feature #1280: Editor: Cell border arrows Feature #1401: Editor: Cloning enhancements Feature #1463: Editor: Fine grained configuration of extended revert/delete commands Feature #1591: Editor: Make fields in creation bar drop targets where applicable Feature #1998: Editor: Magic effect record verifier Feature #1999: Editor Sound Gen record verifier Feature #2000: Editor: Pathgrid record verifier Feature #2528: Game Time Tracker Feature #2534: Editor: global search does not auomatically focus the search input field Feature #2535: OpenMW: allow comments in openmw.cfg Feature #2541: Editor: provide a go to the very bottom button for TopicInfo and JournalInfo Feature #2549: Editor: add a horizontal slider to scroll between opened tables Feature #2558: Editor: provide a shortcut for closing the subview that has the focus Feature #2565: Editor: add context menu for dialogue sub view fields with an item matching "Edit 'x'" from the table subview context menu Feature #2585: Editor: Ignore mouse wheel input for numeric values unless the respective widget has the focus Feature #2620: Editor: make the verify-view refreshable Feature #2622: Editor: Make double click behaviour in result tables configurable (see ID tables) Feature #2717: Editor: Add severity column to report tables Feature #2729: Editor: Various dialogue button bar improvements Feature #2739: Profiling overlay Feature #2740: Resource manager optimizations Feature #2741: Make NIF files into proper resources Feature #2742: Use the skinning data in NIF files as-is Feature #2743: Small feature culling Feature #2744: Configurable near clip distance Feature #2745: GUI scaling option Feature #2747: Support anonymous textures Feature #2749: Loading screen optimizations Feature #2751: Character preview optimization Feature #2804: Editor: Merge Tool Feature #2818: Editor: allow copying a record ID to the clipboard Feature #2946: Editor: add script line number in results of search Feature #2963: Editor: Mouse button bindings in 3D scene Feature #2983: Sun Glare fader Feature #2999: Scaling of journal and books Task #2665: Support building with Qt5 Task #2725: Editor: Remove Display_YesNo Task #2730: Replace hardcoded column numbers in SimpleDialogueSubView/DialogueSubView Task #2750: Bullet shape instancing optimization Task #2793: Replace grid size setting with half grid size setting Task #3003: Support FFMPEG 2.9 (Debian request) 0.36.1 ------ Bug #2590: Start scripts not added correctly 0.36.0 ------ Bug #923: Editor: Operations-Multithreading is broken Bug #1317: Erene Llenim in Seyda Neen does not walk around Bug #1405: Water rendering glitch near Seyda Neen lighthouse Bug #1621: "Error Detecting Morrowind Installation" in the default directory Bug #2216: Creating a clone of the player stops you moving. Bug #2387: Casting bound weapon spell doesn't switch to "ready weapon" mode Bug #2407: Default to (0, 0) when "unknown cell" is encountered. Bug #2411: enchanted item charges don't update/refresh if spell list window is pinned open Bug #2428: Editor: cloning / creating new container class results in invalid omwaddon file - openmw-0.35 Bug #2429: Editor - cloning omits some values or sets different values than the original has Bug #2430: NPC with negative fatigue don't fall (LGNPC Vivec, Foreign Quarter v2.21) Bug #2432: Error on startup with Uvirith's Legacy enabled Bug #2435: Editor: changed entries in the objects window are not shown as such Bug #2437: Editor: changing an entry of a container/NPC/clothing/ingredient/globals will not be saved in the omwaddon file Bug #2447: Editor doesn't save terrain information Bug #2451: Editor not listing files with accented characters Bug #2453: Chargen: sex, race and hair sliders not initialized properly Bug #2459: Minor terrain clipping through statics due to difference in triangle alignment Bug #2461: Invisible sound mark has collision in Sandus Ancestral Tomb Bug #2465: tainted gold stack Bug #2475: cumulative stacks of 100 point fortify skill speechcraft boosts do not apply correctly Bug #2498: Editor: crash when issuing undo command after the table subview is closed Bug #2500: Editor: object table - can't undo delete record Bug #2518: OpenMW detect spell returns false positives Bug #2521: NPCs don't react to stealing when inventory menu is open. Bug #2525: Can't click on red dialogue choice [rise of house telvanni][60fffec] Bug #2530: GetSpellEffects not working as in vanilla Bug #2557: Crash on first launch after choosing "Run installation wizard" Feature #139: Editor: Global Search & Replace Feature #1219: Editor: Add dialogue mode only columns Feature #2024: Hotkey for hand to hand (i.e. unequip any weapon) Feature #2119: "Always Sneak" key bind Feature #2262: Editor: Handle moved instances Feature #2425: Editor: Add start script table Feature #2426: Editor: start script record verifier Feature #2480: Launcher: Multiselect entries in the Data Files list Feature #2505: Editor: optionally show a line number column in the script editor Feature #2512: Editor: Offer use of monospace fonts in the script editor as an option Feature #2514: Editor: focus on ID input field on clone/add Feature #2519: it is not possible to change icons that appear on the map after casting the Detect spells Task #2460: OS X: Use Application Support directory as user data path Task #2516: Editor: Change References / Referenceables terminology 0.35.1 ------ Bug #781: incorrect trajectory of the sun Bug #1079: Wrong starting position in "Character Stuff Wonderland" Bug #1443: Repetitive taking of a stolen object is repetitively considered as a crime Bug #1533: Divine Intervention goes to the wrong place. Bug #1714: No visual indicator for time passed during training Bug #1916: Telekinesis does not allow safe opening of traps Bug #2227: Editor: addon file name inconsistency Bug #2271: Player can melee enemies from water with impunity Bug #2275: Objects with bigger scale move further using Move script Bug #2285: Aryon's Dominator enchantment does not work properly Bug #2290: No punishment for stealing gold from owned containers Bug #2328: Launcher does not respond to Ctrl+C Bug #2334: Drag-and-drop on a content file in the launcher creates duplicate items Bug #2338: Arrows reclaimed from corpses do not stack sometimes Bug #2344: Launcher - Settings importer running correctly? Bug #2346: Launcher - Importing plugins into content list screws up the load order Bug #2348: Mod: H.E.L.L.U.V.A. Handy Holdables does not appear in the content list Bug #2353: Detect Animal detects dead creatures Bug #2354: Cmake does not respect LIB_SUFFIX Bug #2356: Active magic set inactive when switching magic items Bug #2361: ERROR: ESM Error: Previous record contains unread bytes Bug #2382: Switching spells with "next spell" or "previous spell" while holding shift promps delete spell dialog Bug #2388: Regression: Can't toggle map on/off Bug #2392: MOD Shrines - Restore Health and Cancel Options adds 100 health points Bug #2394: List of Data Files tab in openmw-laucher needs to show all content files. Bug #2402: Editor: skills saved incorrectly Bug #2408: Equipping a constant effect Restore Health/Magicka/Fatigue item will permanently boost the stat it's restoring Bug #2415: It is now possible to fall off the prison ship into the water when starting a new game Bug #2419: MOD MCA crash to desktop Bug #2420: Game crashes when character enters a certain area Bug #2421: infinite loop when using cycle weapon without having a weapon Feature #2221: Cannot dress dead NPCs Feature #2349: Check CMake sets correct MSVC compiler settings for release build. Feature #2397: Set default values for global mandatory records. Feature #2412: Basic joystick support 0.35.0 ------ Bug #244: Clipping/static in relation to the ghostgate/fence sound. Bug #531: Missing transparent menu items Bug #811: Content Lists in openmw.cfg are overwritten Bug #925: OpenCS doesn't launch because it thinks its already started Bug #969: Water shader strange behaviour on AMD card Bug #1049: Partially highlighted word in dialogue may cause incorrect line break Bug #1069: omwlauncher.exe crashes due to file lock Bug #1192: It is possible to jump on top of hostile creatures in combat Bug #1342: Loud ambient sounds Bug #1431: Creatures can climb the player Bug #1605: Guard in CharGen doesn't turn around to face you when reaching stairs Bug #1624: Moon edges don't transition properly Bug #1634: Items dropped by PC have collision Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? Bug #1638: Cannot climb staircases Bug #1648: Enchanted equipment badly handled at game reload Bug #1663: Crash when casting spell at enemy near you Bug #1683: Scale doesn't apply to animated collision nodes Bug #1702: Active enchanted item forgotten Bug #1730: Scripts names starting with digit(s) fail to compile Bug #1743: Moons are transparent Bug #1745: Shadows crash: Assertion `mEffects.empty()' failed. Bug #1785: Can't equip two-handed weapon and shield Bug #1809: Player falls too easily Bug #1825: Sword of Perithia can´t run in OpenMW Bug #1899: The launcher resets any alterations you´ve made in the mod list order, Bug #1964: Idle voices/dialogs not triggered correctly Bug #1980: Please, change default click behavior in OpenMW Launchers Data Files list Bug #1984: Vampire corpses standing up when looting the first item Bug #1985: Calm spell does nothing Bug #1986: Spell name lights up on mouseover but spell cost does not Bug #1989: Tooltip still shown when menu toggled off Bug #2010: Raindrops Displayed While Underwater Bug #2023: Walking into plants causes massive framedrop Bug #2031: [MOD: Shrines - Restore Health and Cancel Options]: Restore health option doesn't work Bug #2039: Lake Fjalding pillar of fire not rendered Bug #2040: AI_follow should stop further from the target Bug #2076: Slaughterfish AI Bug #2077: Direction of long jump can be changed much more than it is possible in vanilla Bug #2078: error during rendering: Object '' not found (const) Bug #2105: Lockpicking causes screen sync glitch Bug #2113: [MOD: Julan Ashlander Companion] Julan does not act correctly within the Ghostfence. Bug #2123: Window glow mod: Collision issues Bug #2133: Missing collision for bridges in Balmora when using Morrowind Rebirth 2.81 Bug #2135: Casting a summon spell while the summon is active does not reset the summon. Bug #2144: Changing equipment will unequip drawn arrows/bolts Bug #2169: Yellow on faces when using opengl renderer and mods from overhaul on windows Bug #2175: Pathgrid mods do not overwrite the existing pathgrid Bug #2176: Morrowind -Russian localization end add-on ChaosHeart. Error in framelistener;object ;frenzying toush; not found Bug #2181: Mod Morrowind crafting merchants die. Bug #2182: mods changing skill progression double the bonus for class specialization Bug #2183: Editor: Skills "use value" only allows integer between 0 and 99 Bug #2184: Animated Morrowind Expanded produces an error on Open MW Launch Bug #2185: Conditional Operator formats Bug #2193: Quest: Gateway Ghost Bug #2194: Cannot summon multiples of the same creature Bug #2195: Pathgrid in the (0,0) exterior cell not loaded Bug #2200: Outdoor NPCs can stray away and keep walking into a wall Bug #2201: Creatures do not receive fall damage Bug #2202: The enchantment the item can hold is calculated incorrectly Bug #2203: Having the mod Living Cities of Vvardenfall running causes the game world to fail to load after leaving the prison ship Bug #2204: Abot's Water Life - Book rendered incorrectly Bug #2205: sound_waterfall script no longer compiles Bug #2206: Dialogue script fails to compile (extra .) Bug #2207: Script using – instead of - character does not compile Bug #2208: Failing dialogue scripts in french Morrowind.esm Bug #2214: LGNPC Vivec Redoran 1.62 and The King Rat (Size and inventory Issues) Bug #2215: Beast races can use enchanted boots Bug #2218: Incorrect names body parts in 3D models for open helmet with skinning Bug #2219: Orcs in Ghorak Manor in Caldera don't attack if you pick their pockets. Bug #2220: Chargen race preview head incorrect orientation Bug #2223: Reseting rock falling animation Bug #2224: Fortify Attribute effects do not stack when Spellmaking. Bug #2226: OpenCS pseudo-crash Bug #2230: segfaulting when entering Ald'ruhn with a specific mod: "fermeture la nuit" (closed by night) Bug #2233: Area effect spells on touch do not have the area effect Bug #2234: Dwarven Crossbow clips through the ground when dropped Bug #2235: class SettingsBase<> reverses the order of entries with multiple keys. Bug #2236: Weird two handed longsword + torch interaction Bug #2237: Shooting arrows while sneaking do not agro Bug #2238: Bipedal creatures not using weapons are not handled properly Bug #2245: Incorrect topic highlighting in HT_SpyBaladas quest Bug #2252: Tab completion incomplete for places using COC from the console. Bug #2255: Camera reverts to first person on load Bug #2259: enhancement: the save/load progress bar is not very progressive Bug #2263: TogglePOV can not be bound to Alt key Bug #2267: dialogue disabling via mod Bug #2268: Highlighting Files with load order problems in Data Files tab of Launcher Bug #2276: [Mod]ShotN issues with Karthwasten Bug #2283: Count argument for PlaceAt functions not working Bug #2284: Local map notes should be visible on door marker leading to the cell with the note Bug #2293: There is a graphical glitch at the end of the spell's animation in 3rd Person (looking over the shoulder) view Bug #2294: When using Skyrim UI Overhaul, the tops of pinnable menus are invisible Bug #2302: Random leveled items repeat way too often in a single dungeon Bug #2306: Enchanted arrows should not be retrievable from corpses Bug #2308: No sound effect when drawing the next throwing knife Bug #2309: Guards chase see the player character even if they're invisible Bug #2319: Inverted controls and other issues after becoming a vampire Bug #2324: Spells cast when crossing cell border are imprinted on the local map Bug #2330: Actors with Drain Health effect retain health after dying Bug #2331: tgm (god mode) won't allow the player to cast spells if the player doesn't have enough mana Bug #2332: Error in framelistener: Need a skeleton to attach the arrow to Feature #114: ess-Importer Feature #504: Editor: Delete selected rows from result windows Feature #1024: Addition of remaining equipping hotkeys Feature #1067: Handle NIF interpolation type 4 (XYZ_ROTATION_KEY) Feature #1125: AI fast-forward Feature #1228: Drowning while knocked out Feature #1325: Editor: Opening window and User Settings window cleanup Feature #1537: Ability to change the grid size from 3x3 to 5x5 (or more with good pc) Feature #1546: Leveled list script functions Feature #1659: Test dialogue scripts in --script-all Feature #1720: NPC lookAt controller Feature #2178: Load initial particle system state from NIF files Feature #2197: Editor: When clicking on a script error in the report window set cursor in script editor to the respective line/column Feature #2261: Warn when loading save games with mod mismatch Feature #2313: ess-Importer: convert global map exploration overlay Feature #2318: Add commandline option to load a save game Task #810: Rename "profile" to "content list" Task #2196: Label local/global openmw.cfg files via comments 0.34.0 ------ Bug #904: omwlauncher doesn't allow installing Tribunal and Bloodmoon if only MW is installed Bug #986: Launcher: renaming profile names is broken Bug #1061: "Browse to CD..." launcher crash Bug #1135: Launcher crashes if user does not have write permission Bug #1231: Current installer in launcher does not correctly import russian Morrowind.ini settings from setup.inx Bug #1288: Fix the Alignment of the Resolution Combobox Bug #1343: BIK videos occasionally out of sync with audio Bug #1684: Morrowind Grass Mod graphical glitches Bug #1734: NPC in fight with invisible/sneaking player Bug #1982: Long class names are cut off in the UI Bug #2012: Editor: OpenCS script compiler sometimes fails to find IDs Bug #2015: Running while levitating does not affect speed but still drains fatigue Bug #2018: OpenMW don´t reset modified cells to vanilla when a plugin is deselected and don´t apply changes to cells already visited. Bug #2045: ToggleMenus command should close dialogue windows Bug #2046: Crash: light_de_streetlight_01_223 Bug #2047: Buglamp tooltip minor correction Bug #2050: Roobrush floating texture bits Bug #2053: Slaves react negatively to PC picking up slave's bracers Bug #2055: Dremora corpses use the wrong model Bug #2056: Mansilamat Vabdas's corpse is floating in the water Bug #2057: "Quest: Larius Varro Tells A Little Story": Bounty not completely removed after finishing quest Bug #2059: Silenced enemies try to cast spells anyway Bug #2060: Editor: Special case implementation for top level window with single sub-window should be optional Bug #2061: Editor: SubView closing that is not directly triggered by the user isn't handled properly Bug #2063: Tribunal: Quest 'The Warlords' doesn't work Bug #2064: Sneak attack on hostiles causes bounty Bug #2065: Editor: Qt signal-slot error when closing a dialogue subview Bug #2070: Loading ESP in OpenMW works but fails in OpenCS Bug #2071: CTD in 0.33 Bug #2073: Storm atronach animation stops now and then Bug #2075: Molag Amur Region, Map shows water on solid ground Bug #2080: game won't work with fair magicka regen Bug #2082: NPCs appear frozen or switched off after leaving and quickly reentering a cell Bug #2088: OpenMW is unable to play OGG files. Bug #2093: Darth Gares talks to you in Ilunibi even when he's not there, screwing up the Main Quests Bug #2095: Coordinate and rotation editing in the Reference table does not work. Bug #2096: Some overflow fun and bartering exploit Bug #2098: [D3D] Game crash on maximize Bug #2099: Activate, player seems not to work Bug #2104: Only labels are sensitive in buttons Bug #2107: "Slowfall" effect is too weak Bug #2114: OpenCS doesn't load an ESP file full of errors even though Vanilla MW Construction Set can Bug #2117: Crash when encountering bandits on opposite side of river from the egg mine south of Balmora Bug #2124: [Mod: Baldurians Transparent Glass Amor] Armor above head Bug #2125: Unnamed NiNodes in weapons problem in First Person Bug #2126: Dirty dialog script in tribunal.esm causing bug in Tribunal MQ Bug #2128: Crash when picking character's face Bug #2129: Disable the third-person zoom feature by default Bug #2130: Ash storm particles shown too long during transition to clear sky Bug #2137: Editor: exception caused by following the Creature column of a SoundGen record Bug #2139: Mouse movement should be ignored during intro video Bug #2143: Editor: Saving is broken Bug #2145: OpenMW - crash while exiting x64 debug build Bug #2152: You can attack Almalexia during her final monologue Bug #2154: Visual effects behave weirdly after loading/taking a screenshot Bug #2155: Vivec has too little magicka Bug #2156: Azura's spirit fades away too fast Bug #2158: [Mod]Julan Ashlander Companion 2.0: Negative magicka Bug #2161: Editor: combat/magic/stealth values of creature not displayed correctly Bug #2163: OpenMW can't detect death if the NPC die by the post damage effect of a magic weapon. Bug #2168: Westly's Master Head Pack X – Some hairs aren't rendered correctly. Bug #2170: Mods using conversations to update PC inconsistant Bug #2180: Editor: Verifier doesn't handle Windows-specific path issues when dealing with resources Bug #2212: Crash or unexpected behavior while closing OpenCS cell render window on OS X Feature #238: Add UI to run INI-importer from the launcher Feature #854: Editor: Add user setting to show status bar Feature #987: Launcher: first launch instructions for CD need to be more explicit Feature #1232: There is no way to set the "encoding" option using launcher UI. Feature #1281: Editor: Render cell markers Feature #1918: Editor: Functionality for Double-Clicking in Tables Feature #1966: Editor: User Settings dialogue grouping/labelling/tooltips Feature #2097: Editor: Edit position of references in 3D scene Feature #2121: Editor: Add edit mode button to scene toolbar Task #1965: Editor: Improve layout of user settings dialogue 0.33.1 ------ Bug #2108: OpenCS fails to build 0.33.0 ------ Bug #371: If console assigned to ` (probably to any symbolic key), "`" symbol will be added to console every time it closed Bug #1148: Some books'/scrolls' contents are displayed incorrectly Bug #1290: Editor: status bar is not updated when record filter is changed Bug #1292: Editor: Documents are not removed on closing the last view Bug #1301: Editor: File->Exit only checks the document it was issued from. Bug #1353: Bluetooth on with no speaker connected results in significantly longer initial load times Bug #1436: NPCs react from too far distance Bug #1472: PC is placed on top of following NPC when changing cell Bug #1487: Tall PC can get stuck in staircases Bug #1565: Editor: Subviews are deleted on shutdown instead when they are closed Bug #1623: Door marker on Ghorak Manor's balcony makes PC stuck Bug #1633: Loaddoor to Sadrith Mora, Telvanni Council House spawns PC in the air Bug #1655: Use Appropriate Application Icons on Windows Bug #1679: Tribunal expansion, Meryn Othralas the backstage manager in the theatre group in Mournhold in the great bazaar district is floating a good feet above the ground. Bug #1705: Rain is broken in third person Bug #1706: Thunder and lighting still occurs while the game is paused during the rain Bug #1708: No long jumping Bug #1710: Editor: ReferenceableID drag to references record filter field creates incorrect filter Bug #1712: Rest on Water Bug #1715: "Cancel" button is not always on the same side of menu Bug #1725: Editor: content file can be opened multiple times from the same dialogue Bug #1730: [MOD: Less Generic Nerevarine] Compile failure attempting to enter the Corprusarium. Bug #1733: Unhandled ffmpeg sample formats Bug #1735: Editor: "Edit Record" context menu button not opening subview for journal infos Bug #1750: Editor: record edits result in duplicate entries Bug #1789: Editor: Some characters cannot be used in addon name Bug #1803: Resizing the map does not keep the pre-resize center at the post-resize center Bug #1821: Recovering Cloudcleaver quest: attacking Sosia is considered a crime when you side with Hlormar Bug #1838: Editor: Preferences window appears off screen Bug #1839: Editor: Record filter title should be moved two pixels to the right Bug #1849: Subrecord error in MAO_Containers Bug #1854: Knocked-out actors don't fully act knocked out Bug #1855: "Soul trapped" sound doesn't play Bug #1857: Missing sound effect for enchanted items with empty charge Bug #1859: Missing console command: ResetActors (RA) Bug #1861: Vendor category "MagicItems" is unhandled Bug #1862: Launcher doesn't start if a file listed in launcher.cfg has correct name but wrong capitalization Bug #1864: Editor: Region field for cell record in dialogue subview not working Bug #1869: Editor: Change label "Musics" to "Music" Bug #1870: Goblins killed while knocked down remain in knockdown-pose Bug #1874: CellChanged events should not trigger when crossing exterior cell border Bug #1877: Spriggans killed instantly if hit while regening Bug #1878: Magic Menu text not un-highlighting correctly when going from spell to item as active magic Bug #1881: Stuck in ceiling when entering castle karstaags tower Bug #1884: Unlit torches still produce a burning sound Bug #1885: Can type text in price field in barter window Bug #1887: Equipped items do not emit sounds Bug #1889: draugr lord aesliip will attack you and remain non-hostile Bug #1892: Guard asks player to pay bounty of 0 gold Bug #1895: getdistance should only return max float if ref and target are in different worldspaces Bug #1896: Crash Report Bug #1897: Conjured Equipment cant be re-equipped if removed Bug #1898: Only Gidar Verothan follows you during establish the mine quest Bug #1900: Black screen when you open the door and breath underwater Bug #1904: Crash on casting recall spell Bug #1906: Bound item checks should use the GMSTs Bug #1907: Bugged door. Mournhold, The Winged Guar Bug #1908: Crime reported for attacking Drathas Nerus's henchmen while they attack Dilborn Bug #1909: Weird Quest Flow Infidelities quest Bug #1910: Follower fighting with gone npc Bug #1911: Npcs will drown themselves Bug #1912: World map arrow stays static when inside a building Bug #1920: Ulyne Henim disappears when game is loaded inside Vas Bug #1922: alchemy-> potion of paralyze Bug #1923: "levitation magic cannot be used here" shows outside of tribunal Bug #1927: AI prefer melee over magic. Bug #1929: Tamriel Rebuilt: Named cells that lie within the overlap with Morrowind.esm are not shown Bug #1932: BTB - Spells 14.1 magic effects don´t overwrite the Vanilla ones but are added Bug #1935: Stacks of items are worth more when sold individually Bug #1940: Launcher does not list addon files if base game file is renamed to a different case Bug #1946: Mod "Tel Nechim - moved" breaks savegames Bug #1947: Buying/Selling price doesn't properly affect the growth of mercantile skill Bug #1950: followers from east empire company quest will fight each other if combat happens with anything Bug #1958: Journal can be scrolled indefinitely with a mouse wheel Bug #1959: Follower not leaving party on quest end Bug #1960: Key bindings not always saved correctly Bug #1961: Spell merchants selling racial bonus spells Bug #1967: segmentation fault on load saves Bug #1968: Jump sounds are not controlled by footsteps slider, sound weird compared to footsteps Bug #1970: PC suffers silently when taking damage from lava Bug #1971: Dwarven Sceptre collision area is not removed after killing one Bug #1974: Dalin/Daris Norvayne follows player indefinitely Bug #1975: East Empire Company faction rank breaks during Raven Rock questline Bug #1979: 0 strength = permanently over encumbered Bug #1993: Shrine blessing in Maar Gan doesn't work Bug #2008: Enchanted items do not recharge Bug #2011: Editor: OpenCS script compiler doesn't handle member variable access properly Bug #2016: Dagoth Ur already dead in Facility Cavern Bug #2017: Fighters Guild Quest: The Code Book - dialogue loop when UMP is loaded. Bug #2019: Animation of 'Correct UV Mudcrabs' broken Bug #2022: Alchemy window - Removing ingredient doesn't remove the number of ingredients 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 Feature #879: Editor: Open sub-views in a new top-level window Feature #932: Editor: magic effect table Feature #937: Editor: Path Grid table Feature #938: Editor: Sound Gen table Feature #1117: Death and LevelUp music Feature #1226: Editor: Request UniversalId editing from table columns Feature #1545: Targeting console on player Feature #1597: Editor: Render terrain Feature #1695: Editor: add column for CellRef's global variable Feature #1696: Editor: use ESM::Cell's RefNum counter Feature #1697: Redden player's vision when hit Feature #1856: Spellcasting for non-biped creatures Feature #1879: Editor: Run OpenMW with the currently edited content list Task #1851: Move AI temporary state out of AI packages Task #1865: Replace char type in records 0.32.0 ------ Bug #1132: Unable to jump when facing a wall Bug #1341: Summoned Creatures do not immediately disappear when killed. Bug #1430: CharGen Revamped script does not compile Bug #1451: NPCs shouldn't equip weapons prior to fighting Bug #1461: Stopped start scripts do not restart on load Bug #1473: Dead NPC standing and in 2 pieces Bug #1482: Abilities are depleted when interrupted during casting Bug #1503: Behaviour of NPCs facing the player Bug #1506: Missing character, French edition: three-points Bug #1528: Inventory very slow after 2 hours Bug #1540: Extra arguments should be ignored for script functions Bug #1541: Helseth's Champion: Tribunal Bug #1570: Journal cannot be opened while in inventory screen Bug #1573: PC joins factions at random Bug #1576: NPCs aren't switching their weapons when out of ammo Bug #1579: Guards detect creatures in far distance, instead on sight Bug #1588: The Siege of the Skaal Village: bloodmoon Bug #1593: The script compiler isn't recognising some names that contain a - Bug #1606: Books: Question marks instead of quotation marks Bug #1608: Dead bodies prevent door from opening/closing. Bug #1609: Imperial guards in Sadrith Mora are not using their spears Bug #1610: The bounty number is not displayed properly with high numbers Bug #1620: Implement correct formula for auto-calculated NPC spells Bug #1630: Boats standing vertically in Vivec Bug #1635: Arrest dialogue is executed second time after I select "Go to jail" Bug #1637: Weird NPC behaviour in Vivec, Hlaalu Ancestral Vaults? Bug #1641: Persuasion dialog remains after loading, possibly resulting in crash Bug #1644: "Goodbye" and similar options on dialogues prevents escape working properly. Bug #1646: PC skill stats are not updated immediately when changing equipment Bug #1652: Non-aggressive creature Bug #1653: Quickloading while the container window is open crashes the game Bug #1654: Priority of checks in organic containers Bug #1656: Inventory items merge issue when repairing Bug #1657: Attacked state of NPCs is not saved properly Bug #1660: Rank dialogue condition ignored Bug #1668: Game starts on day 2 instead of day 1 Bug #1669: Critical Strikes while fighting a target who is currently fighting me Bug #1672: OpenCS doesn't save the projects Bug #1673: Fatigue decreasing by only one point when running Bug #1675: Minimap and localmap graphic glitches Bug #1676: Pressing the OK button on the travel menu cancels the travel and exits the menu Bug #1677: Sleeping in a rented bed is considered a crime Bug #1685: NPCs turn towards player even if invisible/sneaking Bug #1686: UI bug: cursor is clicking "world/local" map button while inventory window is closed? Bug #1690: Double clicking on a inventory window header doesn't close it. Bug #1693: Spell Absorption does not absorb shrine blessings Bug #1694: journal displays learned topics as quests Bug #1700: Sideways scroll of text boxes Bug #1701: Player enchanting requires player hold money, always 100% sucessful. Bug #1704: self-made Fortify Intelligence/Drain willpower potions are broken Bug #1707: Pausing the game through the esc menu will silence rain, pausing it by opening the inventory will not. Bug #1709: Remesa Othril is hostile to Hlaalu members Bug #1713: Crash on load after death Bug #1719: Blind effect has slight border at the edge of the screen where it is ineffective. Bug #1722: Crash after creating enchanted item, reloading saved game Bug #1723: Content refs that are stacked share the same index after unstacking Bug #1726: Can't finish Aengoth the Jeweler's quest : Retrieve the Scrap Metal Bug #1727: Targets almost always resist soultrap scrolls Bug #1728: Casting a soultrap spell on invalid target yields no message Bug #1729: Chop attack doesn't work if walking diagonally Bug #1732: Error handling for missing script function arguments produces weird message Bug #1736: Alt-tabbing removes detail from overworld map. Bug #1737: Going through doors with (high magnitude?) leviation will put the player high up, possibly even out of bounds. Bug #1739: Setting a variable on an NPC from another NPC's dialogue result sets the wrong variable Bug #1741: The wait dialogue doesn't black the screen out properly during waiting. Bug #1742: ERROR: Object 'sDifficulty' not found (const) Bug #1744: Night sky in Skies V.IV (& possibly v3) by SWG rendered incorrectly Bug #1746: Bow/marksman weapon condition does not degrade with use Bug #1749: Constant Battle Music Bug #1752: Alt-Tabbing in the character menus makes the paper doll disappear temporarily Bug #1753: Cost of training is not added to merchant's inventory Bug #1755: Disposition changes do not persist if the conversation menu is closed by purchasing training. Bug #1756: Caught Blight after being cured of Corprus Bug #1758: Crash Upon Loading New Cell Bug #1760: Player's Magicka is not recalculated upon drained or boosted intelligence Bug #1761: Equiped torches lost on reload Bug #1762: Your spell did not get a target. Soul trap. Gorenea Andrano Bug #1763: Custom Spell Magicka Cost Bug #1765: Azuras Star breaks on recharging item Bug #1767: GetPCRank did not handle ignored explicit references Bug #1772: Dark Brotherhood Assassins never use their Carved Ebony Dart, sticking to their melee weapon. Bug #1774: String table overflow also occurs when loading TheGloryRoad.esm Bug #1776: dagoth uthol runs in slow motion Bug #1778: Incorrect values in spellmaking window Bug #1779: Icon of Master Propylon Index is not visible Bug #1783: Invisible NPC after looting corpse Bug #1787: Health Calculation Bug #1788: Skeletons, ghosts etc block doors when we try to open Bug #1791: [MOD: LGNPC Foreign Quarter] NPC in completely the wrong place. Bug #1792: Potions should show more effects Bug #1793: Encumbrance while bartering Bug #1794: Fortify attribute not affecting fatigue Bug #1795: Too much magicka Bug #1796: "Off by default" torch burning Bug #1797: Fish too slow Bug #1798: Rest until healed shouldn't show with full health and magicka Bug #1802: Mark location moved Bug #1804: stutter with recent builds Bug #1810: attack gothens dremora doesnt agro the others. Bug #1811: Regression: Crash Upon Loading New Cell Bug #1812: Mod: "QuickChar" weird button placement Bug #1815: Keys show value and weight, Vanilla Morrowind's keys dont. Bug #1817: Persuasion results do not show using unpatched MW ESM Bug #1818: Quest B3_ZainabBride moves to stage 47 upon loading save while Falura Llervu is following Bug #1823: AI response to theft incorrect - only guards react, in vanilla everyone does. Bug #1829: On-Target Spells Rendered Behind Water Surface Effects Bug #1830: Galsa Gindu's house is on fire Bug #1832: Fatal Error: OGRE Exception(2:InvalidParametersException) Bug #1836: Attacked Guards open "fine/jail/resist"-dialogue after killing you Bug #1840: Infinite recursion in ActionTeleport Bug #1843: Escorted people change into player's cell after completion of escort stage Bug #1845: Typing 'j' into 'Name' fields opens the journal Bug #1846: Text pasted into the console still appears twice (Windows) Bug #1847: "setfatigue 0" doesn't render NPC unconscious Bug #1848: I can talk to unconscious actors Bug #1866: Crash when player gets killed by a creature summoned by him Bug #1868: Memory leaking when openmw window is minimized Feature #47: Magic Effects Feature #642: Control NPC mouth movement using current Say sound Feature #939: Editor: Resources tables Feature #961: AI Combat for magic (spells, potions and enchanted items) Feature #1111: Collision script instructions (used e.g. by Lava) Feature #1120: Command creature/humanoid magic effects Feature #1121: Elemental shield magic effects Feature #1122: Light magic effect Feature #1139: AI: Friendly hits Feature #1141: AI: combat party Feature #1326: Editor: Add tooltips to all graphical buttons Feature #1489: Magic effect Get/Mod/Set functions Feature #1505: Difficulty slider Feature #1538: Targeted scripts Feature #1571: Allow creating custom markers on the local map Feature #1615: Determine local variables from compiled scripts instead of the values in the script record Feature #1616: Editor: Body part record verifier Feature #1651: Editor: Improved keyboard navigation for scene toolbar Feature #1666: Script blacklisting Feature #1711: Including the Git revision number from the command line "--version" switch. Feature #1721: NPC eye blinking Feature #1740: Scene toolbar buttons for selecting which type of elements are rendered Feature #1790: Mouse wheel scrolling for the journal Feature #1850: NiBSPArrayController Task #768: On windows, settings folder should be "OpenMW", not "openmw" Task #908: Share keyframe data Task #1716: Remove defunct option for building without FFmpeg 0.31.0 ------ Bug #245: Cloud direction and weather systems differ from Morrowind Bug #275: Local Map does not always show objects that span multiple cells Bug #538: Update CenterOnCell (COC) function behavior Bug #618: Local and World Map Textures are sometimes Black Bug #640: Water behaviour at night Bug #668: OpenMW doesn't support non-latin paths on Windows Bug #746: OpenMW doesn't check if the background music was already played Bug #747: Door is stuck if cell is left before animation finishes Bug #772: Disabled statics are visible on map Bug #829: OpenMW uses up all available vram, when playing for extended time Bug #869: Dead bodies don't collide with anything Bug #894: Various character creation issues Bug #897/#1369: opencs Segmentation Fault after "new" or "load" Bug #899: Various jumping issues Bug #952: Reflection effects are one frame delayed Bug #993: Able to interact with world during Wait/Rest dialog Bug #995: Dropped items can be placed inside the wall Bug #1008: Corpses always face up upon reentering the cell Bug #1035: Random colour patterns appearing in automap Bug #1037: Footstep volume issues Bug #1047: Creation of wrong links in dialogue window Bug #1129: Summoned creature time life duration seems infinite Bug #1134: Crimes can be committed against hostile NPCs Bug #1136: Creature run speed formula is incorrect Bug #1150: Weakness to Fire doesn't apply to Fire Damage in the same spell Bug #1155: NPCs killing each other Bug #1166: Bittercup script still does not work Bug #1178: .bsa file names are case sensitive. Bug #1179: Crash after trying to load game after being killed Bug #1180: Changing footstep sound location Bug #1196: Jumping not disabled when showing messageboxes Bug #1202: "strange" keys are not shown in binding menu, and are not saved either, but works Bug #1216: Broken dialog topics in russian Morrowind Bug #1217: Container content changes based on the current position of the mouse Bug #1234: Loading/saving issues with dynamic records Bug #1277: Text pasted into the console appears twice Bug #1284: Crash on New Game Bug #1303: It's possible to skip the chargen Bug #1304: Slaughterfish should not detect the player unless the player is in the water Bug #1311: Editor: deleting Record Filter line does not reset the filter Bug #1324: ERROR: ESM Error: String table overflow when loading Animated Morrowind.esp Bug #1328: Editor: Bogus Filter created when dragging multiple records to filter bar of non-applicable table Bug #1331: Walking/running sound persist after killing NPC`s that are walking/running. Bug #1334: Previously equipped items not shown as unequipped after attempting to sell them. Bug #1335: Actors ignore vertical axis when deciding to attack Bug #1338: Unknown toggle option for shadows Bug #1339: "Ashlands Region" is visible when beginning new game during "Loading Area" process Bug #1340: Guards prompt Player with punishment options after resisting arrest with another guard. Bug #1348: Regression: Bug #1098 has returned with a vengeance Bug #1349: [TR] TR_Data mesh tr_ex_imp_gatejamb01 cannot be activated Bug #1352: Disabling an ESX file does not disable dependent ESX files Bug #1355: CppCat Checks OpenMW Bug #1356: Incorrect voice type filtering for sleep interrupts Bug #1357: Restarting the game clears saves Bug #1360: Seyda Neen silk rider dialog problem Bug #1361: Some lights don't work Bug #1364: It is difficult to bind "Mouse 1" to an action in the options menu Bug #1370: Animation compilation mod does not work properly Bug #1371: SL_Pick01.nif from third party fails to load in openmw, but works in Vanilla Bug #1373: When stealing in front of Sellus Gravius cannot exit the dialog Bug #1378: Installs to /usr/local are not working Bug #1380: Loading a save file fail if one of the content files is disabled Bug #1382: "getHExact() size mismatch" crash on loading official plugin "Siege at Firemoth.esp" Bug #1386: Arkngthand door will not open Bug #1388: Segfault when modifying View Distance in Menu options Bug #1389: Crash when loading a save after dying Bug #1390: Apostrophe characters not displayed [French version] Bug #1391: Custom made icon background texture for magical weapons and stuff isn't scaled properly on GUI. Bug #1393: Coin icon during the level up dialogue are off of the background Bug #1394: Alt+F4 doesn't work on Win version Bug #1395: Changing rings switches only the last one put on Bug #1396: Pauldron parts aren't showing when the robe is equipped Bug #1402: Dialogue of some shrines have wrong button orientation Bug #1403: Items are floating in the air when they're dropped onto dead bodies. Bug #1404: Forearms are not rendered on Argonian females Bug #1407: Alchemy allows making potions from two of the same item Bug #1408: "Max sale" button gives you all the items AND all the trader's gold Bug #1409: Rest "Until Healed" broken for characters with stunted magicka. Bug #1412: Empty travel window opens while playing through start game Bug #1413: Save game ignores missing writing permission Bug #1414: The Underground 2 ESM Error Bug #1416: Not all splash screens in the Splash directory are used Bug #1417: Loading saved game does not terminate Bug #1419: Skyrim: Home of the Nords error Bug #1422: ClearInfoActor Bug #1423: ForceGreeting closes existing dialogue windows Bug #1425: Cannot load save game Bug #1426: Read skill books aren't stored in savegame Bug #1427: Useless items can be set under hotkeys Bug #1429: Text variables in journal Bug #1432: When attacking friendly NPC, the crime is reported and bounty is raised after each swing Bug #1435: Stealing priceless items is without punishment Bug #1437: Door marker at Jobasha's Rare Books is spawning PC in the air Bug #1440: Topic selection menu should be wider Bug #1441: Dropping items on the rug makes them inaccessible Bug #1442: When dropping and taking some looted items, bystanders consider that as a crime Bug #1444: Arrows and bolts are not dropped where the cursor points Bug #1445: Security trainers offering acrobatics instead Bug #1447: Character dash not displayed, French edition Bug #1448: When the player is killed by the guard while having a bounty on his head, the guard dialogue opens over and over instead of loading dialogue Bug #1454: Script error in SkipTutorial Bug #1456: Bad lighting when using certain Morrowind.ini generated by MGE Bug #1457: Heart of Lorkan comes after you when attacking it Bug #1458: Modified Keybindings are not remembered Bug #1459: Dura Gra-Bol doesn't respond to PC attack Bug #1462: Interior cells not loaded with Morrowind Patch active Bug #1469: Item tooltip should show the base value, not real value Bug #1477: Death count is not stored in savegame Bug #1478: AiActivate does not trigger activate scripts Bug #1481: Weapon not rendered when partially submerged in water Bug #1483: Enemies are attacking even while dying Bug #1486: ESM Error: Don't know what to do with INFO Bug #1490: Arrows shot at PC can end up in inventory Bug #1492: Monsters respawn on top of one another Bug #1493: Dialogue box opens with follower NPC even if NPC is dead Bug #1494: Paralysed cliffracers remain airbourne Bug #1495: Dialogue box opens with follower NPC even the game is paused Bug #1496: GUI messages are not cleared when loading another saved game Bug #1499: Underwater sound sometimes plays when transitioning from interior. Bug #1500: Targetted spells and water. Bug #1502: Console error message on info refusal Bug #1507: Bloodmoon MQ The Ritual of Beasts: Can't remove the arrow Bug #1508: Bloodmoon: Fort Frostmoth, cant talk with Carnius Magius Bug #1516: PositionCell doesn't move actors to current cell Bug #1518: ForceGreeting broken for explicit references Bug #1522: Crash after attempting to play non-music file Bug #1523: World map empty after loading interior save Bug #1524: Arrows in waiting/resting dialog act like minimum and maximum buttons Bug #1525: Werewolf: Killed NPC's don't fill werewolfs hunger for blood Bug #1527: Werewolf: Detect life detects wrong type of actor Bug #1529: OpenMW crash during "the shrine of the dead" mission (tribunal) Bug #1530: Selected text in the console has the same color as the background Bug #1539: Barilzar's Mazed Band: Tribunal Bug #1542: Looping taunts from NPC`s after death: Tribunal Bug #1543: OpenCS crash when using drag&drop in script editor Bug #1547: Bamz-Amschend: Centurion Archers combat problem Bug #1548: The Missing Hand: Tribunal Bug #1549: The Mad God: Tribunal, Dome of Serlyn Bug #1557: A bounty is calculated from actual item cost Bug #1562: Invisible terrain on top of Red Mountain Bug #1564: Cave of the hidden music: Bloodmoon Bug #1567: Editor: Deleting of referenceables does not work Bug #1568: Picking up a stack of items and holding the enter key and moving your mouse around paints a bunch of garbage on screen. Bug #1574: Solstheim: Drauger cant inflict damage on player Bug #1578: Solstheim: Bonewolf running animation not working Bug #1585: Particle effects on PC are stopped when paralyzed Bug #1589: Tribunal: Crimson Plague quest does not update when Gedna Relvel is killed Bug #1590: Failed to save game: compile error Bug #1598: Segfault when making Drain/Fortify Skill spells Bug #1599: Unable to switch to fullscreen Bug #1613: Morrowind Rebirth duplicate objects / vanilla objects not removed Bug #1618: Death notice fails to show up Bug #1628: Alt+Tab Segfault Feature #32: Periodic Cleanup/Refill Feature #41: Precipitation and weather particles Feature #568: Editor: Configuration setup Feature #649: Editor: Threaded loading Feature #930: Editor: Cell record saving Feature #934: Editor: Body part table Feature #935: Editor: Enchantment effect table Feature #1162: Dialogue merging Feature #1174: Saved Game: add missing creature state Feature #1177: Saved Game: fog of war state Feature #1312: Editor: Combat/Magic/Stealth values for creatures are not displayed Feature #1314: Make NPCs and creatures fight each other Feature #1315: Crime: Murder Feature #1321: Sneak skill enhancements Feature #1323: Handle restocking items Feature #1332: Saved Game: levelled creatures Feature #1347: modFactionReaction script instruction Feature #1362: Animated main menu support Feature #1433: Store walk/run toggle Feature #1449: Use names instead of numbers for saved game files and folders Feature #1453: Adding Delete button to the load menu Feature #1460: Enable Journal screen while in dialogue Feature #1480: Play Battle music when in combat Feature #1501: Followers unable to fast travel with you Feature #1520: Disposition and distance-based aggression/ShouldAttack Feature #1595: Editor: Object rendering in cells Task #940: Move license to locations where applicable Task #1333: Remove cmake git tag reading Task #1566: Editor: Object rendering refactoring 0.30.0 ------ Bug #416: Extreme shaking can occur during cell transitions while moving Bug #1003: Province Cyrodiil: Ogre Exception in Stirk Bug #1071: Crash when given a non-existent content file Bug #1080: OpenMW allows resting/using a bed while in combat Bug #1097: Wrong punishment for stealing in Census and Excise Office at the start of a new game Bug #1098: Unlocked evidence chests should get locked after new evidence is put into them Bug #1099: NPCs that you attacked still fight you after you went to jail/paid your fine Bug #1100: Taking items from a corpse is considered stealing Bug #1126: Some creatures can't get close enough to attack Bug #1144: Killed creatures seem to die again each time player transitions indoors/outdoors Bug #1181: loading a saved game does not reset the player control status Bug #1185: Collision issues in Addamasartus Bug #1187: Athyn Sarethi mission, rescuing varvur sarethi from the doesnt end the mission Bug #1189: Crash when entering interior cell "Gnisis, Arvs-Drelen" Bug #1191: Picking up papers without inventory in new game Bug #1195: NPCs do not equip torches in certain interiors Bug #1197: mouse wheel makes things scroll too fast Bug #1200: door blocked by monsters Bug #1201: item's magical charges are only refreshed when they are used Bug #1203: Scribs do not defend themselves Bug #1204: creatures life is not empty when they are dead Bug #1205: armor experience does not progress when hits are taken Bug #1206: blood particules always red. Undeads and mechanicals should have a different one. Bug #1209: Tarhiel never falls Bug #1210: journal adding script is ran again after having saved/loaded Bug #1224: Names of custom classes are not properly handled in save games Bug #1227: Editor: Fixed case handling for broken localised versions of Morrowind.esm Bug #1235: Indoors walk stutter Bug #1236: Aborting intro movie brings up the menu Bug #1239: NPCs get stuck when walking past each other Bug #1240: BTB - Settings 14.1 and Health Bar. Bug #1241: BTB - Character and Khajiit Prejudice Bug #1248: GUI Weapon icon is changed to hand-to-hand after save load Bug #1254: Guild ranks do not show in dialogue Bug #1255: When opening a container and selecting "Take All", the screen flashes blue Bug #1260: Level Up menu doesn't show image when using a custom class Bug #1265: Quit Menu Has Misaligned Buttons Bug #1270: Active weapon icon is not updated when weapon is repaired Bug #1271: NPC Stuck in hovering "Jumping" animation Bug #1272: Crash when attempting to load Big City esm file. Bug #1276: Editor: Dropping a region into the filter of a cell subview fails Bug #1286: Dialogue topic list clips with window frame Bug #1291: Saved game: store faction membership Bug #1293: Pluginless Khajiit Head Pack by ashiraniir makes OpenMW close. Bug #1294: Pasting in console adds text to end, not at cursor Bug #1295: Conversation loop when asking about "specific place" in Vivec Bug #1296: Caius doesn't leave at start of quest "Mehra Milo and the Lost Prophecies" Bug #1297: Saved game: map markers Bug #1302: ring_keley script causes vector::_M_range_check exception Bug #1309: Bug on "You violated the law" dialog Bug #1319: Creatures sometimes rendered incorrectly Feature #50: Ranged Combat Feature #58: Sneaking Skill Feature #73: Crime and Punishment Feature #135: Editor: OGRE integration Feature #541: Editor: Dialogue Sub-Views Feature #853: Editor: Rework User Settings Feature #944: Editor: lighting modes Feature #945: Editor: Camera navigation mode Feature #953: Trader gold Feature #1140: AI: summoned creatures Feature #1142: AI follow: Run stance Feature #1154: Not all NPCs get aggressive when one is attacked Feature #1169: Terrain threading Feature #1172: Loading screen and progress bars during saved/loading game Feature #1173: Saved Game: include weather state Feature #1207: Class creation form does not remember Feature #1220: Editor: Preview Subview Feature #1223: Saved Game: Local Variables Feature #1229: Quicksave, quickload, autosave Feature #1230: Deleting saves Feature #1233: Bribe gold is placed into NPCs inventory Feature #1252: Saved Game: quick key bindings Feature #1273: Editor: Region Map context menu Feature #1274: Editor: Region Map drag & drop Feature #1275: Editor: Scene subview drop Feature #1282: Non-faction member crime recognition. Feature #1289: NPCs return to default position Task #941: Remove unused cmake files 0.29.0 ------ Bug #556: Video soundtrack not played when music volume is set to zero Bug #829: OpenMW uses up all available vram, when playing for extended time Bug #848: Wrong amount of footsteps playing in 1st person Bug #888: Ascended Sleepers have movement issues Bug #892: Explicit references are allowed on all script functions Bug #999: Graphic Herbalism (mod): sometimes doesn't activate properly Bug #1009: Lake Fjalding AI related slowdown. Bug #1041: Music playback issues on OS X >= 10.9 Bug #1043: No message box when advancing skill "Speechcraft" while in dialog window Bug #1060: Some message boxes are cut off at the bottom Bug #1062: Bittercup script does not work ('end' variable) Bug #1074: Inventory paperdoll obscures armour rating Bug #1077: Message after killing an essential NPC disappears too fast Bug #1078: "Clutterbane" shows empty charge bar Bug #1083: UndoWerewolf fails Bug #1088: Better Clothes Bloodmoon Plus 1.5 by Spirited Treasure pants are not rendered Bug #1090: Start scripts fail when going to a non-predefined cell Bug #1091: Crash: Assertion `!q.isNaN() && "Invalid orientation supplied as parameter"' failed. Bug #1093: Weapons of aggressive NPCs are invisible after you exit and re-enter interior Bug #1105: Magicka is depleted when using uncastable spells Bug #1106: Creatures should be able to run Bug #1107: TR cliffs have way too huge collision boxes in OpenMW Bug #1109: Cleaning True Light and Darkness with Tes3cmd makes Addamasartus , Zenarbael and Yasamsi flooded. Bug #1114: Bad output for desktop-file-validate on openmw.desktop (and opencs.desktop) Bug #1115: Memory leak when spying on Fargoth Bug #1137: Script execution fails (drenSlaveOwners script) Bug #1143: Mehra Milo quest (vivec informants) is broken Bug #1145: Issues with moving gold between inventory and containers Bug #1146: Issues with picking up stacks of gold Bug #1147: Dwemer Crossbows are held incorrectly Bug #1158: Armor rating should always stay below inventory mannequin Bug #1159: Quick keys can be set during character generation Bug #1160: Crash on equip lockpick when Bug #1167: Editor: Referenceables are not correctly loaded when dealing with more than one content file Bug #1184: Game Save: overwriting an existing save does not actually overwrites the file Feature #30: Loading/Saving (still missing a few parts) Feature #101: AI Package: Activate Feature #103: AI Package: Follow, FollowCell Feature #138: Editor: Drag & Drop Feature #428: Player death Feature #505: Editor: Record Cloning Feature #701: Levelled creatures Feature #708: Improved Local Variable handling Feature #709: Editor: Script verifier Feature #764: Missing journal backend features Feature #777: Creature weapons/shields Feature #789: Editor: Referenceable record verifier Feature #924: Load/Save GUI (still missing loading screen and progress bars) Feature #946: Knockdown Feature #947: Decrease fatigue when running, swimming and attacking Feature #956: Melee Combat: Blocking Feature #957: Area magic Feature #960: Combat/AI combat for creatures Feature #962: Combat-Related AI instructions Feature #1075: Damage/Restore skill/attribute magic effects Feature #1076: Soultrap magic effect Feature #1081: Disease contraction Feature #1086: Blood particles Feature #1092: Interrupt resting Feature #1101: Inventory equip scripts Feature #1116: Version/Build number in Launcher window Feature #1119: Resistance/weakness to normal weapons magic effect Feature #1123: Slow Fall magic effect Feature #1130: Auto-calculate spells Feature #1164: Editor: Case-insensitive sorting in tables 0.28.0 ------ Bug #399: Inventory changes are not visible immediately Bug #417: Apply weather instantly when teleporting Bug #566: Global Map position marker not updated for interior cells Bug #712: Looting corpse delay Bug #716: Problem with the "Vurt's Ascadian Isles Mod" mod Bug #805: Two TR meshes appear black (v0.24RC) Bug #841: Third-person activation distance taken from camera rather than head Bug #845: NPCs hold torches during the day Bug #855: Vvardenfell Visages Volume I some hairs don´t appear since 0,24 Bug #856: Maormer race by Mac Kom - The heads are way up Bug #864: Walk locks during loading in 3rd person Bug #871: active weapon/magic item icon is not immediately made blank if item is removed during dialog Bug #882: Hircine's Ring doesn't always work Bug #909: [Tamriel Rebuilt] crashes in Akamora Bug #922: Launcher writing merged openmw.cfg files Bug #943: Random magnitude should be calculated per effect Bug #948: Negative fatigue level should be allowed Bug #949: Particles in world space Bug #950: Hard crash on x64 Linux running --new-game (on startup) Bug #951: setMagicka and setFatigue have no effect Bug #954: Problem with equipping inventory items when using a keyboard shortcut Bug #955: Issues with equipping torches Bug #966: Shield is visible when casting spell Bug #967: Game crashes when equipping silver candlestick Bug #970: Segmentation fault when starting at Bal Isra Bug #977: Pressing down key in console doesn't go forward in history Bug #979: Tooltip disappears when changing inventory Bug #980: Barter: item category is remembered, but not shown Bug #981: Mod: replacing model has wrong position/orientation Bug #982: Launcher: Addon unchecking is not saved Bug #983: Fix controllers to affect objects attached to the base node Bug #985: Player can talk to NPCs who are in combat Bug #989: OpenMW crashes when trying to include mod with capital .ESP Bug #991: Merchants equip items with harmful constant effect enchantments Bug #994: Don't cap skills/attributes when set via console Bug #998: Setting the max health should also set the current health Bug #1005: Torches are visible when casting spells and during hand to hand combat. Bug #1006: Many NPCs have 0 skill Bug #1007: Console fills up with text Bug #1013: Player randomly loses health or dies Bug #1014: Persuasion window is not centered in maximized window Bug #1015: Player status window scroll state resets on status change Bug #1016: Notification window not big enough for all skill level ups Bug #1020: Saved window positions are not rescaled appropriately on resolution change Bug #1022: Messages stuck permanently on screen when they pile up Bug #1023: Journals doesn't open Bug #1026: Game loses track of torch usage. Bug #1028: Crash on pickup of jug in Unexplored Shipwreck, Upper level Bug #1029: Quick keys menu: Select compatible replacement when tool used up Bug #1042: TES3 header data wrong encoding Bug #1045: OS X: deployed OpenCS won't launch Bug #1046: All damaged weaponry is worth 1 gold Bug #1048: Links in "locked" dialogue are still clickable Bug #1052: Using color codes when naming your character actually changes the name's color Bug #1054: Spell effects not visible in front of water Bug #1055: Power-Spell animation starts even though you already casted it that day Bug #1059: Cure disease potion removes all effects from player, even your race bonus and race ability Bug #1063: Crash upon checking out game start ship area in Seyda Neen Bug #1064: openmw binaries link to unnecessary libraries Bug #1065: Landing from a high place in water still causes fall damage Bug #1072: Drawing weapon increases torch brightness Bug #1073: Merchants sell stacks of gold Feature #43: Visuals for Magic Effects Feature #51: Ranged Magic Feature #52: Touch Range Magic Feature #53: Self Range Magic Feature #54: Spell Casting Feature #70: Vampirism Feature #100: Combat AI Feature #171: Implement NIF record NiFlipController Feature #410: Window to restore enchanted item charge Feature #647: Enchanted item glow Feature #723: Invisibility/Chameleon magic effects Feature #737: Resist Magicka magic effect Feature #758: GetLOS Feature #926: Editor: Info-Record tables Feature #958: Material controllers Feature #959: Terrain bump, specular, & parallax mapping Feature #990: Request: unlock mouse when in any menu Feature #1018: Do not allow view mode switching while performing an action Feature #1027: Vertex morph animation (NiGeomMorpherController) Feature #1031: Handle NiBillboardNode Feature #1051: Implement NIF texture slot DarkTexture Task #873: Unify OGRE initialisation 0.27.0 ------ Bug #597: Assertion `dialogue->mId == id' failed in esmstore.cpp Bug #794: incorrect display of decimal numbers Bug #840: First-person sneaking camera height Bug #887: Ambient sounds playing while paused Bug #902: Problems with Polish character encoding Bug #907: Entering third person using the mousewheel is possible even if it's impossible using the key Bug #910: Some CDs not working correctly with Unshield installer Bug #917: Quick character creation plugin does not work Bug #918: Fatigue does not refill Bug #919: The PC falls dead in Beshara - OpenMW nightly Win64 (708CDE2) Feature #57: Acrobatics Skill Feature #462: Editor: Start Dialogue Feature #546: Modify ESX selector to handle new content file scheme Feature #588: Editor: Adjust name/path of edited content files Feature #644: Editor: Save Feature #710: Editor: Configure script compiler context Feature #790: God Mode Feature #881: Editor: Allow only one instance of OpenCS Feature #889: Editor: Record filtering Feature #895: Extinguish torches Feature #898: Breath meter enhancements Feature #901: Editor: Default record filter Feature #913: Merge --master and --plugin switches 0.26.0 ------ Bug #274: Inconsistencies in the terrain Bug #557: Already-dead NPCs do not equip clothing/items. Bug #592: Window resizing Bug #612: [Tamriel Rebuilt] Missing terrain (South of Tel Oren) Bug #664: Heart of lorkhan acts like a dead body (container) Bug #767: Wonky ramp physics & water Bug #780: Swimming out of water Bug #792: Wrong ground alignment on actors when no clipping Bug #796: Opening and closing door sound issue Bug #797: No clipping hinders opening and closing of doors Bug #799: sliders in enchanting window Bug #838: Pressing key during startup procedure freezes the game Bug #839: Combat/magic stances during character creation Bug #843: [Tribunal] Dark Brotherhood assassin appears without equipment Bug #844: Resting "until healed" option given even with full stats Bug #846: Equipped torches are invisible. Bug #847: Incorrect formula for autocalculated NPC initial health Bug #850: Shealt weapon sound plays when leaving magic-ready stance Bug #852: Some boots do not produce footstep sounds Bug #860: FPS bar misalignment Bug #861: Unable to print screen Bug #863: No sneaking and jumping at the same time Bug #866: Empty variables in [Movies] section of Morrowind.ini gets imported into OpenMW.cfg as blank fallback option and crashes game on start. Bug #867: Dancing girls in "Suran, Desele's House of Earthly Delights" don't dance. Bug #868: Idle animations are repeated Bug #874: Underwater swimming close to the ground is jerky Bug #875: Animation problem while swimming on the surface and looking up Bug #876: Always a starting upper case letter in the inventory Bug #878: Active spell effects don't update the layout properly when ended Bug #891: Cell 24,-12 (Tamriel Rebuilt) crashes on load Bug #896: New game sound issue Feature #49: Melee Combat Feature #71: Lycanthropy Feature #393: Initialise MWMechanics::AiSequence from ESM::AIPackageList Feature #622: Multiple positions for inventory window Feature #627: Drowning Feature #786: Allow the 'Activate' key to close the countdialog window Feature #798: Morrowind installation via Launcher (Linux/Max OS only) Feature #851: First/Third person transitions with mouse wheel Task #689: change PhysicActor::enableCollisions Task #707: Reorganise Compiler 0.25.0 ------ Bug #411: Launcher crash on OS X < 10.8 Bug #604: Terrible performance drop in the Census and Excise Office. Bug #676: Start Scripts fail to load Bug #677: OpenMW does not accept script names with - Bug #766: Extra space in front of topic links Bug #793: AIWander Isn't Being Passed The Repeat Parameter Bug #795: Sound playing with drawn weapon and crossing cell-border Bug #800: can't select weapon for enchantment Bug #801: Player can move while over-encumbered Bug #802: Dead Keys not working Bug #808: mouse capture Bug #809: ini Importer does not work without an existing cfg file Bug #812: Launcher will run OpenMW with no ESM or ESP selected Bug #813: OpenMW defaults to Morrowind.ESM with no ESM or ESP selected Bug #817: Dead NPCs and Creatures still have collision boxes Bug #820: Incorrect sorting of answers (Dialogue) Bug #826: mwinimport dumps core when given an unknown parameter Bug #833: getting stuck in door Bug #835: Journals/books not showing up properly. Feature #38: SoundGen Feature #105: AI Package: Wander Feature #230: 64-bit compatibility for OS X Feature #263: Hardware mouse cursors Feature #449: Allow mouse outside of window while paused Feature #736: First person animations Feature #750: Using mouse wheel in third person mode Feature #822: Autorepeat for slider buttons 0.24.0 ------ Bug #284: Book's text misalignment Bug #445: Camera able to get slightly below floor / terrain Bug #582: Seam issue in Red Mountain Bug #632: Journal Next Button shows white square Bug #653: IndexedStore ignores index Bug #694: Parser does not recognize float values starting with . Bug #699: Resource handling broken with Ogre 1.9 trunk Bug #718: components/esm/loadcell is using the mwworld subsystem Bug #729: Levelled item list tries to add nonexistent item Bug #730: Arrow buttons in the settings menu do not work. Bug #732: Erroneous behavior when binding keys Bug #733: Unclickable dialogue topic Bug #734: Book empty line problem Bug #738: OnDeath only works with implicit references Bug #740: Script compiler fails on scripts with special names Bug #742: Wait while no clipping Bug #743: Problem with changeweather console command Bug #744: No wait dialogue after starting a new game Bug #748: Player is not able to unselect objects with the console Bug #751: AddItem should only spawn a message box when called from dialogue Bug #752: The enter button has several functions in trade and looting that is not impelemted. Bug #753: Fargoth's Ring Quest Strange Behavior Bug #755: Launcher writes duplicate lines into settings.cfg Bug #759: Second quest in mages guild does not work Bug #763: Enchantment cast cost is wrong Bug #770: The "Take" and "Close" buttons in the scroll GUI are stretched incorrectly Bug #773: AIWander Isn't Being Passed The Correct idle Values Bug #778: The journal can be opened at the start of a new game Bug #779: Divayth Fyr starts as dead Bug #787: "Batch count" on detailed FPS counter gets cut-off Bug #788: chargen scroll layout does not match vanilla Feature #60: Atlethics Skill Feature #65: Security Skill Feature #74: Interaction with non-load-doors Feature #98: Render Weapon and Shield Feature #102: AI Package: Escort, EscortCell Feature #182: Advanced Journal GUI Feature #288: Trading enhancements Feature #405: Integrate "new game" into the menu Feature #537: Highlight dialogue topic links Feature #658: Rotate, RotateWorld script instructions and local rotations Feature #690: Animation Layering Feature #722: Night Eye/Blind magic effects Feature #735: Move, MoveWorld script instructions. Feature #760: Non-removable corpses 0.23.0 ------ Bug #522: Player collides with placeable items Bug #553: Open/Close sounds played when accessing main menu w/ Journal Open Bug #561: Tooltip word wrapping delay Bug #578: Bribing works incorrectly Bug #601: PositionCell fails on negative coordinates Bug #606: Some NPCs hairs not rendered with Better Heads addon Bug #609: Bad rendering of bone boots Bug #613: Messagebox causing assert to fail Bug #631: Segfault on shutdown Bug #634: Exception when talking to Calvus Horatius in Mournhold, royal palace courtyard Bug #635: Scale NPCs depending on race Bug #643: Dialogue Race select function is inverted Bug #646: Twohanded weapons don't work properly Bug #654: Crash when dropping objects without a collision shape Bug #655/656: Objects that were disabled or deleted (but not both) were added to the scene when re-entering a cell Bug #660: "g" in "change" cut off in Race Menu Bug #661: Arrille sells me the key to his upstairs room Bug #662: Day counter starts at 2 instead of 1 Bug #663: Cannot select "come unprepared" topic in dialog with Dagoth Ur Bug #665: Pickpocket -> "Grab all" grabs all NPC inventory, even not listed in container window. Bug #666: Looking up/down problem Bug #667: Active effects border visible during loading Bug #669: incorrect player position at new game start Bug #670: race selection menu: sex, face and hair left button not totally clickable Bug #671: new game: player is naked Bug #674: buying or selling items doesn't change amount of gold Bug #675: fatigue is not set to its maximum when starting a new game Bug #678: Wrong rotation order causes RefData's rotation to be stored incorrectly Bug #680: different gold coins in Tel Mara Bug #682: Race menu ignores playable flag for some hairs and faces Bug #685: Script compiler does not accept ":" after a function name Bug #688: dispose corpse makes cross-hair to disappear Bug #691: Auto equipping ignores equipment conditions Bug #692: OpenMW doesnt load "loose file" texture packs that places resources directly in data folder Bug #696: Draugr incorrect head offset Bug #697: Sail transparency issue Bug #700: "On the rocks" mod does not load its UV coordinates correctly. Bug #702: Some race mods don't work Bug #711: Crash during character creation Bug #715: Growing Tauryon Bug #725: Auto calculate stats Bug #728: Failure to open container and talk dialogue Bug #731: Crash with Mush-Mere's "background" topic Feature #55/657: Item Repairing Feature #62/87: Enchanting Feature #99: Pathfinding Feature #104: AI Package: Travel Feature #129: Levelled items Feature #204: Texture animations Feature #239: Fallback-Settings Feature #535: Console object selection improvements Feature #629: Add levelup description in levelup layout dialog Feature #630: Optional format subrecord in (tes3) header Feature #641: Armor rating Feature #645: OnDeath script function Feature #683: Companion item UI Feature #698: Basic Particles Task #648: Split up components/esm/loadlocks Task #695: mwgui cleanup 0.22.0 ------ Bug #311: Potential infinite recursion in script compiler Bug #355: Keyboard repeat rate (in Xorg) are left disabled after game exit. Bug #382: Weird effect in 3rd person on water Bug #387: Always use detailed shape for physics raycasts Bug #420: Potion/ingredient effects do not stack Bug #429: Parts of dwemer door not picked up correctly for activation/tooltips Bug #434/Bug #605: Object movement between cells not properly implemented Bug #502: Duplicate player collision model at origin Bug #509: Dialogue topic list shifts inappropriately Bug #513: Sliding stairs Bug #515: Launcher does not support non-latin strings Bug #525: Race selection preview camera wrong position Bug #526: Attributes / skills should not go below zero Bug #529: Class and Birthsign menus options should be preselected Bug #530: Lock window button graphic missing Bug #532: Missing map menu graphics Bug #545: ESX selector does not list ESM files properly Bug #547: Global variables of type short are read incorrectly Bug #550: Invisible meshes collision and tooltip Bug #551: Performance drop when loading multiple ESM files Bug #552: Don't list CG in options if it is not available Bug #555: Character creation windows "OK" button broken Bug #558: Segmentation fault when Alt-tabbing with console opened Bug #559: Dialog window should not be available before character creation is finished Bug #560: Tooltip borders should be stretched Bug #562: Sound should not be played when an object cannot be picked up Bug #565: Water animation speed + timescale Bug #572: Better Bodies' textures don't work Bug #573: OpenMW doesn't load if TR_Mainland.esm is enabled (Tamriel Rebuilt mod) Bug #574: Moving left/right should not cancel auto-run Bug #575: Crash entering the Chamber of Song Bug #576: Missing includes Bug #577: Left Gloves Addon causes ESMReader exception Bug #579: Unable to open container "Kvama Egg Sack" Bug #581: Mimicking vanilla Morrowind water Bug #583: Gender not recognized Bug #586: Wrong char gen behaviour Bug #587: "End" script statements with spaces don't work Bug #589: Closing message boxes by pressing the activation key Bug #590: Ugly Dagoth Ur rendering Bug #591: Race selection issues Bug #593: Persuasion response should be random Bug #595: Footless guard Bug #599: Waterfalls are invisible from a certain distance Bug #600: Waterfalls rendered incorrectly, cut off by water Bug #607: New beast bodies mod crashes Bug #608: Crash in cell "Mournhold, Royal Palace" Bug #611: OpenMW doesn't find some of textures used in Tamriel Rebuilt Bug #613: Messagebox causing assert to fail Bug #615: Meshes invisible from above water Bug #617: Potion effects should be hidden until discovered Bug #619: certain moss hanging from tree has rendering bug Bug #621: Batching bloodmoon's trees Bug #623: NiMaterialProperty alpha unhandled Bug #628: Launcher in latest master crashes the game Bug #633: Crash on startup: Better Heads Bug #636: Incorrect Char Gen Menu Behavior Feature #29: Allow ESPs and multiple ESMs Feature #94: Finish class selection-dialogue Feature #149: Texture Alphas Feature #237: Run Morrowind-ini importer from launcher Feature #286: Update Active Spell Icons Feature #334: Swimming animation Feature #335: Walking animation Feature #360: Proper collision shapes for NPCs and creatures Feature #367: Lights that behave more like original morrowind implementation Feature #477: Special local scripting variables Feature #528: Message boxes should close when enter is pressed under certain conditions. Feature #543: Add bsa files to the settings imported by the ini importer Feature #594: coordinate space and utility functions Feature #625: Zoom in vanity mode Task #464: Refactor launcher ESX selector into a re-usable component Task #624: Unified implementation of type-variable sub-records 0.21.0 ------ Bug #253: Dialogs don't work for Russian version of Morrowind Bug #267: Activating creatures without dialogue can still activate the dialogue GUI Bug #354: True flickering lights Bug #386: The main menu's first entry is wrong (in french) Bug #479: Adding the spell "Ash Woe Blight" to the player causes strange attribute oscillations Bug #495: Activation Range Bug #497: Failed Disposition check doesn't stop a dialogue entry from being returned Bug #498: Failing a disposition check shouldn't eliminate topics from the the list of those available Bug #500: Disposition for most NPCs is 0/100 Bug #501: Getdisposition command wrongly returns base disposition Bug #506: Journal UI doesn't update anymore Bug #507: EnableRestMenu is not a valid command - change it to EnableRest Bug #508: Crash in Ald Daedroth Shrine Bug #517: Wrong price calculation when untrading an item Bug #521: MWGui::InventoryWindow creates a duplicate player actor at the origin Bug #524: Beast races are able to wear shoes Bug #527: Background music fails to play Bug #533: The arch at Gnisis entrance is not displayed Bug #534: Terrain gets its correct shape only some time after the cell is loaded Bug #536: The same entry can be added multiple times to the journal Bug #539: Race selection is broken Bug #544: Terrain normal map corrupt when the map is rendered Feature #39: Video Playback Feature #151: ^-escape sequences in text output Feature #392: Add AI related script functions Feature #456: Determine required ini fallback values and adjust the ini importer accordingly Feature #460: Experimental DirArchives improvements Feature #540: Execute scripts of objects in containers/inventories in active cells Task #401: Review GMST fixing Task #453: Unify case smashing/folding Task #512: Rewrite utf8 component 0.20.0 ------ Bug #366: Changing the player's race during character creation does not change the look of the player character Bug #430: Teleporting and using loading doors linking within the same cell reloads the cell Bug #437: Stop animations when paused Bug #438: Time displays as "0 a.m." when it should be "12 a.m." Bug #439: Text in "name" field of potion/spell creation window is persistent Bug #440: Starting date at a new game is off by one day Bug #442: Console window doesn't close properly sometimes Bug #448: Do not break container window formatting when item names are very long Bug #458: Topics sometimes not automatically added to known topic list Bug #476: Auto-Moving allows player movement after using DisablePlayerControls Bug #478: After sleeping in a bed the rest dialogue window opens automtically again Bug #492: On creating potions the ingredients are removed twice Feature #63: Mercantile skill Feature #82: Persuasion Dialogue Feature #219: Missing dialogue filters/functions Feature #369: Add a FailedAction Feature #377: Select head/hair on character creation Feature #391: Dummy AI package classes Feature #435: Global Map, 2nd Layer Feature #450: Persuasion Feature #457: Add more script instructions Feature #474: update the global variable pcrace when the player's race is changed Task #158: Move dynamically generated classes from Player class to World Class Task #159: ESMStore rework and cleanup Task #163: More Component Namespace Cleanup Task #402: Move player data from MWWorld::Player to the player's NPC record Task #446: Fix no namespace in BulletShapeLoader 0.19.0 ------ Bug #374: Character shakes in 3rd person mode near the origin Bug #404: Gamma correct rendering Bug #407: Shoes of St. Rilm do not work Bug #408: Rugs has collision even if they are not supposed to Bug #412: Birthsign menu sorted incorrectly Bug #413: Resolutions presented multiple times in launcher Bug #414: launcher.cfg file stored in wrong directory Bug #415: Wrong esm order in openmw.cfg Bug #418: Sound listener position updates incorrectly Bug #423: wrong usage of "Version" entry in openmw.desktop Bug #426: Do not use hardcoded splash images Bug #431: Don't use markers for raycast Bug #432: Crash after picking up items from an NPC Feature #21/#95: Sleeping/resting Feature #61: Alchemy Skill Feature #68: Death Feature #69/#86: Spell Creation Feature #72/#84: Travel Feature #76: Global Map, 1st Layer Feature #120: Trainer Window Feature #152: Skill Increase from Skill Books Feature #160: Record Saving Task #400: Review GMST access 0.18.0 ------ Bug #310: Button of the "preferences menu" are too small Bug #361: Hand-to-hand skill is always 100 Bug #365: NPC and creature animation is jerky; Characters float around when they are not supposed to Bug #372: playSound3D uses original coordinates instead of current coordinates. Bug #373: Static OGRE build faulty Bug #375: Alt-tab toggle view Bug #376: Screenshots are disable Bug #378: Exception when drinking self-made potions Bug #380: Cloth visibility problem Bug #384: Weird character on doors tooltip. Bug #398: Some objects do not collide in MW, but do so in OpenMW Feature #22: Implement level-up Feature #36: Hide Marker Feature #88: Hotkey Window Feature #91: Level-Up Dialogue Feature #118: Keyboard and Mouse-Button bindings Feature #119: Spell Buying Window Feature #133: Handle resources across multiple data directories Feature #134: Generate a suitable default-value for --data-local Feature #292: Object Movement/Creation Script Instructions Feature #340: AIPackage data structures Feature #356: Ingredients use Feature #358: Input system rewrite Feature #370: Target handling in actions Feature #379: Door markers on the local map Feature #389: AI framework Feature #395: Using keys to open doors / containers Feature #396: Loading screens Feature #397: Inventory avatar image and race selection head preview Task #339: Move sounds into Action 0.17.0 ------ Bug #225: Valgrind reports about 40MB of leaked memory Bug #241: Some physics meshes still don't match Bug #248: Some textures are too dark Bug #300: Dependency on proprietary CG toolkit Bug #302: Some objects don't collide although they should Bug #308: Freeze in Balmora, Meldor: Armorer Bug #313: openmw without a ~/.config/openmw folder segfault. Bug #317: adding non-existing spell via console locks game Bug #318: Wrong character normals Bug #341: Building with Ogre Debug libraries does not use debug version of plugins Bug #347: Crash when running openmw with --start="XYZ" Bug #353: FindMyGUI.cmake breaks path on Windows Bug #359: WindowManager throws exception at destruction Bug #364: Laggy input on OS X due to bug in Ogre's event pump implementation Feature #33: Allow objects to cross cell-borders Feature #59: Dropping Items (replaced stopgap implementation with a proper one) Feature #93: Main Menu Feature #96/329/330/331/332/333: Player Control Feature #180: Object rotation and scaling. Feature #272: Incorrect NIF material sharing Feature #314: Potion usage Feature #324: Skill Gain Feature #342: Drain/fortify dynamic stats/attributes magic effects Feature #350: Allow console only script instructions Feature #352: Run scripts in console on startup Task #107: Refactor mw*-subsystems Task #325: Make CreatureStats into a class Task #345: Use Ogre's animation system Task #351: Rewrite Action class to support automatic sound playing 0.16.0 ------ Bug #250: OpenMW launcher erratic behaviour Bug #270: Crash because of underwater effect on OS X Bug #277: Auto-equipping in some cells not working Bug #294: Container GUI ignores disabled inventory menu Bug #297: Stats review dialog shows all skills and attribute values as 0 Bug #298: MechanicsManager::buildPlayer does not remove previous bonuses Bug #299: Crash in World::disable Bug #306: Non-existent ~/.config/openmw "crash" the launcher. Bug #307: False "Data Files" location make the launcher "crash" Feature #81: Spell Window Feature #85: Alchemy Window Feature #181: Support for x.y script syntax Feature #242: Weapon and Spell icons Feature #254: Ingame settings window Feature #293: Allow "stacking" game modes Feature #295: Class creation dialog tooltips Feature #296: Clicking on the HUD elements should show/hide the respective window Feature #301: Direction after using a Teleport Door Feature #303: Allow object selection in the console Feature #305: Allow the use of = as a synonym for == Feature #312: Compensation for slow object access in poorly written Morrowind.esm scripts Task #176: Restructure enabling/disabling of MW-references Task #283: Integrate ogre.cfg file in settings file Task #290: Auto-Close MW-reference related GUI windows 0.15.0 ------ Bug #5: Physics reimplementation (fixes various issues) Bug #258: Resizing arrow's background is not transparent Bug #268: Widening the stats window in X direction causes layout problems Bug #269: Topic pane in dialgoue window is too small for some longer topics Bug #271: Dialog choices are sorted incorrectly Bug #281: The single quote character is not rendered on dialog windows Bug #285: Terrain not handled properly in cells that are not predefined Bug #289: Dialogue filter isn't doing case smashing/folding for item IDs Feature #15: Collision with Terrain Feature #17: Inventory-, Container- and Trade-Windows Feature #44: Floating Labels above Focussed Objects Feature #80: Tooltips Feature #83: Barter Dialogue Feature #90: Book and Scroll Windows Feature #156: Item Stacking in Containers Feature #213: Pulsating lights Feature #218: Feather & Burden Feature #256: Implement magic effect bookkeeping Feature #259: Add missing information to Stats window Feature #260: Correct case for dialogue topics Feature #280: GUI texture atlasing Feature #291: Ability to use GMST strings from GUI layout files Task #255: Make MWWorld::Environment into a singleton 0.14.0 ------ Bug #1: Meshes rendered with wrong orientation Bug #6/Task #220: Picking up small objects doesn't always work Bug #127: tcg doesn't work Bug #178: Compablity problems with Ogre 1.8.0 RC 1 Bug #211: Wireframe mode (toggleWireframe command) should not apply to Console & other UI Bug #227: Terrain crashes when moving away from predefined cells Bug #229: On OS X Launcher cannot launch game if path to binary contains spaces Bug #235: TGA texture loading problem Bug #246: wireframe mode does not work in water Feature #8/#232: Water Rendering Feature #13: Terrain Rendering Feature #37: Render Path Grid Feature #66: Factions Feature #77: Local Map Feature #78: Compass/Mini-Map Feature #97: Render Clothing/Armour Feature #121: Window Pinning Feature #205: Auto equip Feature #217: Contiainer should track changes to its content Feature #221: NPC Dialogue Window Enhancements Feature #233: Game settings manager Feature #240: Spell List and selected spell (no GUI yet) Feature #243: Draw State Task #113: Morrowind.ini Importer Task #215: Refactor the sound code Task #216: Update MyGUI 0.13.0 ------ Bug #145: Fixed sound problems after cell change Bug #179: Pressing space in console triggers activation Bug #186: CMake doesn't use the debug versions of Ogre libraries on Linux Bug #189: ASCII 16 character added to console on it's activation on Mac OS X Bug #190: Case Folding fails with music files Bug #192: Keypresses write Text into Console no matter which gui element is active Bug #196: Collision shapes out of place Bug #202: ESMTool doesn't not work with localised ESM files anymore Bug #203: Torch lights only visible on short distance Bug #207: Ogre.log not written Bug #209: Sounds do not play Bug #210: Ogre crash at Dren plantation Bug #214: Unsupported file format version Bug #222: Launcher is writing openmw.cfg file to wrong location Feature #9: NPC Dialogue Window Feature #16/42: New sky/weather implementation Feature #40: Fading Feature #48: NPC Dialogue System Feature #117: Equipping Items (backend only, no GUI yet, no rendering of equipped items yet) Feature #161: Load REC_PGRD records Feature #195: Wireframe-mode Feature #198/199: Various sound effects Feature #206: Allow picking data path from launcher if non is set Task #108: Refactor window manager class Task #172: Sound Manager Cleanup Task #173: Create OpenEngine systems in the appropriate manager classes Task #184: Adjust MSVC and gcc warning levels Task #185: RefData rewrite Task #201: Workaround for transparency issues Task #208: silenced esm_reader.hpp warning 0.12.0 ------ Bug #154: FPS Drop Bug #169: Local scripts continue running if associated object is deleted Bug #174: OpenMW fails to start if the config directory doesn't exist Bug #187: Missing lighting Bug #188: Lights without a mesh are not rendered Bug #191: Taking screenshot causes crash when running installed Feature #28: Sort out the cell load problem Feature #31: Allow the player to move away from pre-defined cells Feature #35: Use alternate storage location for modified object position Feature #45: NPC animations Feature #46: Creature Animation Feature #89: Basic Journal Window Feature #110: Automatically pick up the path of existing MW-installations Feature #183: More FPS display settings Task #19: Refactor engine class Task #109/Feature #162: Automate Packaging Task #112: Catch exceptions thrown in input handling functions Task #128/#168: Cleanup Configuration File Handling Task #131: NPC Activation doesn't work properly Task #144: MWRender cleanup Task #155: cmake cleanup 0.11.1 ------ Bug #2: Resources loading doesn't work outside of bsa files Bug #3: GUI does not render non-English characters Bug #7: openmw.cfg location doesn't match Bug #124: The TCL alias for ToggleCollision is missing. Bug #125: Some command line options can't be used from a .cfg file Bug #126: Toggle-type script instructions are less verbose compared with original MW Bug #130: NPC-Record Loading fails for some NPCs Bug #167: Launcher sets invalid parameters in ogre config Feature #10: Journal Feature #12: Rendering Optimisations Feature #23: Change Launcher GUI to a tabbed interface Feature #24: Integrate the OGRE settings window into the launcher Feature #25: Determine openmw.cfg location (Launcher) Feature #26: Launcher Profiles Feature #79: MessageBox Feature #116: Tab-Completion in Console Feature #132: --data-local and multiple --data Feature #143: Non-Rendering Performance-Optimisations Feature #150: Accessing objects in cells via ID does only work for objects with all lower case IDs Feature #157: Version Handling Task #14: Replace tabs with 4 spaces Task #18: Move components from global namespace into their own namespace Task #123: refactor header files in components/esm 0.10.0 ------ * NPC dialogue window (not functional yet) * Collisions with objects * Refactor the PlayerPos class * Adjust file locations * CMake files and test linking for Bullet * Replace Ogre raycasting test for activation with something more precise * Adjust player movement according to collision results * FPS display * Various Portability Improvements * Mac OS X support is back! 0.9.0 ----- * Exterior cells loading, unloading and management * Character Creation GUI * Character creation * Make cell names case insensitive when doing internal lookups * Music player * NPCs rendering 0.8.0 ----- * GUI * Complete and working script engine * In game console * Sky rendering * Sound and music * Tons of smaller stuff 0.7.0 ----- * This release is a complete rewrite in C++. * All D code has been culled, and all modules have been rewritten. * The game is now back up to the level of rendering interior cells and moving around, but physics, sound, GUI, and scripting still remain to be ported from the old codebase. 0.6.0 ----- * Coded a GUI system using MyGUI * Skinned MyGUI to look like Morrowind (work in progress) * Integrated the Monster script engine * Rewrote some functions into script code * Very early MyGUI < > Monster binding * Fixed Windows sound problems (replaced old openal32.dll) 0.5.0 ----- * Collision detection with Bullet * Experimental walk & fall character physics * New key bindings: * t toggle physics mode (walking, flying, ghost), * n night eye, brightens the scene * Fixed incompatability with DMD 1.032 and newer compilers * * (thanks to tomqyp) * Various minor changes and updates 0.4.0 ----- * Switched from Audiere to OpenAL * * (BIG thanks to Chris Robinson) * Added complete Makefile (again) as a alternative build tool * More realistic lighting (thanks again to Chris Robinson) * Various localization fixes tested with Russian and French versions * Temporary workaround for the Unicode issue: invalid UTF displayed as '?' * Added ns option to disable sound, for debugging * Various bug fixes * Cosmetic changes to placate gdc Wall 0.3.0 ----- * Built and tested on Windows XP * Partial support for FreeBSD (exceptions do not work) * You no longer have to download Monster separately * Made an alternative for building without DSSS (but DSSS still works) * Renamed main program from 'morro' to 'openmw' * Made the config system more robust * Added oc switch for showing Ogre config window on startup * Removed some config files, these are auto generated when missing. * Separated plugins.cfg into linux and windows versions. * Updated Makefile and sources for increased portability * confirmed to work against OIS 1.0.0 (Ubuntu repository package) 0.2.0 ----- * Compiles with gdc * Switched to DSSS for building D code * Includes the program esmtool 0.1.0 ----- first release openmw-openmw-0.49.0/CI/000077500000000000000000000000001503074453300147135ustar00rootroot00000000000000openmw-openmw-0.49.0/CI/ActivateMSVC.ps1000066400000000000000000000015701503074453300175740ustar00rootroot00000000000000& "${env:COMSPEC}" /c ActivateMSVC.bat "&&" set | ForEach-Object { if ($_.Contains("=")) { $name, $value = $_ -split '=', 2 Set-Content env:\"$name" $value } } $MissingTools = $false $tools = "cl", "link", "rc", "mt" $descriptions = "MSVC Compiler", "MSVC Linker", "MS Windows Resource Compiler", "MS Windows Manifest Tool" for ($i = 0; $i -lt $tools.Length; $i++) { $present = $true try { Get-Command $tools[$i] *>&1 | Out-Null $present = $present -and $? } catch { $present = $false } if (!$present) { Write-Warning "$($tools[$i]) ($($descriptions[$i])) missing." $MissingTools = $true } } if ($MissingTools) { Write-Error "Some build tools were unavailable after activating MSVC in the shell. It's likely that your Visual Studio $MSVC_DISPLAY_YEAR installation needs repairing." exit 1 }openmw-openmw-0.49.0/CI/Store-Symbols.ps1000066400000000000000000000043111503074453300200610ustar00rootroot00000000000000param ( [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." openmw-openmw-0.49.0/CI/activate_msvc.sh000066400000000000000000000031111503074453300200730ustar00rootroot00000000000000#!/bin/bash oldSettings=$- set -eu function restoreOldSettings { if [[ $oldSettings != *e* ]]; then set +e fi if [[ $oldSettings != *u* ]]; then set +u fi } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then echo "Error: Script not sourced." echo "You must source this script for it to work, i.e. " echo "source ./activate_msvc.sh" echo "or" echo ". ./activate_msvc.sh" restoreOldSettings exit 1 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 } # capture CMD environment in a shell with MSVC activated cmd //c "$(unixPathAsWindows "$(dirname "${BASH_SOURCE[0]}")")\ActivateMSVC.bat" "&&" "bash" "-c" "declare -px > declared_env.sh" source ./declared_env.sh rm declared_env.sh MISSINGTOOLS=0 command -v cl >/dev/null 2>&1 || { echo "Error: cl (MSVC Compiler) missing."; MISSINGTOOLS=1; } command -v link >/dev/null 2>&1 || { echo "Error: link (MSVC Linker) missing."; MISSINGTOOLS=1; } command -v rc >/dev/null 2>&1 || { echo "Error: rc (MS Windows Resource Compiler) missing."; MISSINGTOOLS=1; } command -v mt >/dev/null 2>&1 || { echo "Error: mt (MS Windows Manifest Tool) missing."; MISSINGTOOLS=1; } if [ $MISSINGTOOLS -ne 0 ]; then echo "Some build tools were unavailable after activating MSVC in the shell. It's likely that your Visual Studio $MSVC_DISPLAY_YEAR installation needs repairing." restoreOldSettings return 1 fi restoreOldSettings openmw-openmw-0.49.0/CI/before_install.android.sh000077500000000000000000000004071503074453300216620ustar00rootroot00000000000000#!/bin/sh -ex 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 openmw-openmw-0.49.0/CI/before_install.macos.sh000077500000000000000000000003431503074453300213430ustar00rootroot00000000000000#!/bin/sh -ex export HOMEBREW_NO_EMOJI=1 export HOMEBREW_NO_INSTALL_CLEANUP=1 export HOMEBREW_AUTOREMOVE=1 if [[ "${MACOS_AMD64}" ]]; then ./CI/macos/before_install.amd64.sh else ./CI/macos/before_install.arm64.sh fi openmw-openmw-0.49.0/CI/before_script.android.sh000077500000000000000000000027211503074453300215210ustar00rootroot00000000000000#!/bin/sh -ex # 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=/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 \ -DBUILD_BSATOOL=0 \ -DBUILD_NIFTEST=0 \ -DBUILD_ESMTOOL=0 \ -DBUILD_LAUNCHER=0 \ -DBUILD_MWINIIMPORTER=0 \ -DBUILD_ESSIMPORTER=0 \ -DBUILD_OPENCS=0 \ -DBUILD_WIZARD=0 \ -DBUILD_NAVMESHTOOL=OFF \ -DBUILD_BULLETOBJECTTOOL=OFF \ -DOPENMW_USE_SYSTEM_MYGUI=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" \ .. openmw-openmw-0.49.0/CI/before_script.linux.sh000077500000000000000000000062411503074453300212410ustar00rootroot00000000000000#!/bin/bash set -xeo pipefail free -m # 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 -DBUILD_SHARED_LIBS="${BUILD_SHARED_LIBS:-OFF}" -DUSE_SYSTEM_TINYXML=ON -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=ON -DOPENMW_CXX_FLAGS="${OPENMW_CXX_FLAGS}" # 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 -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 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 \ -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 \ -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 \ "${CMAKE_CONF_OPTS[@]}" \ .. fi openmw-openmw-0.49.0/CI/before_script.macos.sh000077500000000000000000000036541503074453300212110ustar00rootroot00000000000000#!/bin/sh -e # Silence a git warning git config --global advice.detachedHead false rm -fr build mkdir build cd build DEPENDENCIES_ROOT="/tmp/openmw-deps" if [[ "${MACOS_AMD64}" ]]; then QT_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix qt@6) ICU_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix icu4c) OPENAL_PATH=$(arch -x86_64 /usr/local/bin/brew --prefix openal-soft) CCACHE_EXECUTABLE=$(arch -x86_64 /usr/local/bin/brew --prefix ccache)/bin/ccache else QT_PATH=$(brew --prefix qt@6) ICU_PATH=$(brew --prefix icu4c) OPENAL_PATH=$(brew --prefix openal-soft) CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache fi declare -a CMAKE_CONF_OPTS=( -D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH" -D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" -D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" -D CMAKE_CXX_FLAGS="-stdlib=libc++" -D CMAKE_C_COMPILER="clang" -D CMAKE_CXX_COMPILER="clang++" -D CMAKE_OSX_DEPLOYMENT_TARGET="13.6" -D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE -D Boost_INCLUDE_DIR="$DEPENDENCIES_ROOT/include" -D OSGPlugins_LIB_DIR="$DEPENDENCIES_ROOT/lib/osgPlugins-3.6.5" -D ICU_ROOT="$ICU_PATH" -D OPENMW_OSX_DEPLOYMENT=TRUE ) declare -a BUILD_OPTS=( -D BUILD_OPENMW=TRUE -D BUILD_OPENCS=TRUE -D BUILD_ESMTOOL=TRUE -D BUILD_BSATOOL=TRUE -D BUILD_ESSIMPORTER=TRUE -D BUILD_NIFTEST=TRUE -D BUILD_NAVMESHTOOL=TRUE -D BUILD_BULLETOBJECTTOOL=TRUE -G"Unix Makefiles" ) if [[ "${MACOS_AMD64}" ]]; then CMAKE_CONF_OPTS+=( -D CMAKE_OSX_ARCHITECTURES="x86_64" ) fi if [[ "${CMAKE_BUILD_TYPE}" ]]; then CMAKE_CONF_OPTS+=( -D CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} ) else CMAKE_CONF_OPTS+=( -D CMAKE_BUILD_TYPE=RelWithDebInfo ) fi if [[ "${MACOS_AMD64}" ]]; then arch -x86_64 cmake \ "${CMAKE_CONF_OPTS[@]}" \ "${BUILD_OPTS[@]}" \ .. else cmake \ "${CMAKE_CONF_OPTS[@]}" \ "${BUILD_OPTS[@]}" \ .. fi openmw-openmw-0.49.0/CI/before_script.msvc.sh000066400000000000000000000510501503074453300210450ustar00rootroot00000000000000#!/bin/bash # set -x # turn-on for debugging function wrappedExit { if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then exit $1 else return $1 fi } 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; } if [ $MISSINGTOOLS -ne 0 ]; then wrappedExit 1 fi WORKINGDIR="$(pwd)" case "$WORKINGDIR" in *[[:space:]]*) echo "Error: Working directory contains spaces." wrappedExit 1 ;; esac set -euo pipefail function windowsPathAsUnix { if command -v cygpath >/dev/null 2>&1; then cygpath -u $1 else echo "$1" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1," fi } function unixPathAsWindows { if command -v cygpath >/dev/null 2>&1; then cygpath -w $1 else echo "$1" | sed "s,^/\([^/]\)/,\\1:/," | sed "s,/,\\\\,g" fi } CI=${CI:-} STEP=${STEP:-} VERBOSE="" STRIP="" SKIP_DOWNLOAD="" SKIP_EXTRACT="" USE_CCACHE="" KEEP="" UNITY_BUILD="" VS_VERSION="" NMAKE="" NINJA="" PDBS="" PLATFORM="" CONFIGURATIONS=() TEST_FRAMEWORK="" INSTALL_PREFIX="." BUILD_BENCHMARKS="" USE_WERROR="" USE_CLANG_TIDY="" ACTIVATE_MSVC="" SINGLE_CONFIG="" while [ $# -gt 0 ]; do ARGSTR=$1 shift if [ ${ARGSTR:0:1} != "-" ]; then echo "Unknown argument $ARGSTR" echo "Try '$0 -h'" wrappedExit 1 fi for (( i=1; i<${#ARGSTR}; i++ )); do ARG=${ARGSTR:$i:1} case $ARG in V ) VERBOSE=true ;; d ) SKIP_DOWNLOAD=true ;; e ) SKIP_EXTRACT=true ;; C ) USE_CCACHE=true ;; k ) KEEP=true ;; u ) UNITY_BUILD=true ;; v ) VS_VERSION=$1 shift ;; n ) NMAKE=true ;; N ) NINJA=true ;; p ) PLATFORM=$1 shift ;; P ) PDBS=true ;; c ) CONFIGURATIONS+=( $1 ) shift ;; t ) TEST_FRAMEWORK=true ;; i ) INSTALL_PREFIX=$(echo "$1" | sed 's;\\;/;g' | sed -E 's;/+;/;g') shift ;; b ) BUILD_BENCHMARKS=true ;; E ) USE_WERROR=true ;; T ) USE_CLANG_TIDY=true ;; h ) cat < Set the configuration, can also be set with environment variable CONFIGURATION. For mutli-config generators, this is ignored, and all configurations are set up. For single-config generators, several configurations can be set up at once by specifying -c multiple times. -C Use ccache. -d Skip checking the downloads. -e Skip extracting dependencies. -h Show this message. -k Keep the old build directory, default is to delete it. -p Set the build platform, can also be set with environment variable PLATFORM. -t Build unit tests / Google test -u Configure for unity builds. -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. -N Produce Ninja (multi-config if CMake is new enough to support it) files instead of a Visual Studio solution. Cannot be used with -n.. -P Download debug symbols where available -V Run verbosely -i 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 ;; * ) echo "Unknown argument $ARG." echo "Try '$0 -h'" wrappedExit 1 ;; esac done done if [ -n "$NMAKE" ] || [ -n "$NINJA" ]; then if [ -n "$NMAKE" ] && [ -n "$NINJA" ]; then echo "Cannot run in NMake and Ninja mode at the same time." wrappedExit 1 fi ACTIVATE_MSVC=true fi if [ -z $VERBOSE ]; then STRIP="> /dev/null 2>&1" fi DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}") cd $(dirname "$DIR")/.. run_cmd() { CMD="$1" shift if [ -z $VERBOSE ]; then RET=0 eval $CMD $@ > output.log 2>&1 || RET=$? if [ $RET -ne 0 ]; then echo "Command $CMD failed, output can be found in $(real_pwd)/output.log" else rm output.log fi return $RET else RET=0 eval $CMD $@ || RET=$? return $RET fi } download() { if [ $# -lt 3 ]; then echo "Invalid parameters to download." return 1 fi NAME=$1 shift echo "$NAME..." while [ $# -gt 1 ]; do URL=$1 FILE=$2 shift shift if ! [ -f $FILE ]; then printf " Downloading $FILE... " if [ -z $VERBOSE ]; then RET=0 curl --silent --fail --retry 10 -Ly 5 -o $FILE $URL || RET=$? else RET=0 curl --fail --retry 10 -Ly 5 -o $FILE $URL || RET=$? fi if [ $RET -ne 0 ]; then echo "Failed!" wrappedExit $RET else echo "Done." fi else echo " $FILE exists, skipping." fi done if [ $# -ne 0 ]; then echo "Missing parameter." fi } MANIFEST_FILE="" download_from_manifest() { if [ $# -ne 1 ]; then echo "Invalid parameters to download_from_manifest." return 1 fi { read -r URL && read -r HASH FILE; } < $1 if [ -z $SKIP_DOWNLOAD ]; then download "${FILE:?}" "${URL:?}" "${FILE:?}" fi echo "${HASH:?} ${FILE:?}" | sha512sum --check MANIFEST_FILE="${FILE:?}" } real_pwd() { if type cygpath >/dev/null 2>&1; then cygpath -am "$PWD" else pwd # not git bash, Cygwin or the like fi } CMAKE_OPTS="" add_cmake_opts() { CMAKE_OPTS="$CMAKE_OPTS $@" } declare -A RUNTIME_DLLS RUNTIME_DLLS["Release"]="" RUNTIME_DLLS["Debug"]="" RUNTIME_DLLS["RelWithDebInfo"]="" add_runtime_dlls() { local CONFIG=$1 shift RUNTIME_DLLS[$CONFIG]="${RUNTIME_DLLS[$CONFIG]} $@" } declare -A OSG_PLUGINS OSG_PLUGINS["Release"]="" OSG_PLUGINS["Debug"]="" OSG_PLUGINS["RelWithDebInfo"]="" add_osg_dlls() { local CONFIG=$1 shift OSG_PLUGINS[$CONFIG]="${OSG_PLUGINS[$CONFIG]} $@" } declare -A QT_PLATFORMS QT_PLATFORMS["Release"]="" QT_PLATFORMS["Debug"]="" QT_PLATFORMS["RelWithDebInfo"]="" add_qt_platform_dlls() { local CONFIG=$1 shift QT_PLATFORMS[$CONFIG]="${QT_PLATFORMS[$CONFIG]} $@" } declare -A QT_STYLES QT_STYLES["Release"]="" QT_STYLES["Debug"]="" QT_STYLES["RelWithDebInfo"]="" add_qt_style_dlls() { local CONFIG=$1 shift 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="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" MSVC_TOOLSET="vc142" MSVC_REAL_VER="16" MSVC_DISPLAY_YEAR="2019" QT_MSVC_YEAR="2019" ;; 15|15.0|2017 ) echo "Visual Studio 2017 is no longer supported" wrappedExit 1 ;; 14|14.0|2015 ) echo "Visual Studio 2015 is no longer supported" wrappedExit 1 ;; 12|12.0|2013 ) echo "Visual Studio 2013 is no longer supported" wrappedExit 1 ;; esac case $PLATFORM in x64|x86_64|x86-64|win64|Win64 ) ARCHNAME="x86-64" ARCHSUFFIX="64" BITS="64" ;; x32|x86|i686|i386|win32|Win32 ) ARCHNAME="x86" ARCHSUFFIX="86" BITS="32" ;; * ) echo "Unknown platform $PLATFORM." wrappedExit 1 ;; esac if [ -n "$NMAKE" ]; then GENERATOR="NMake Makefiles" SINGLE_CONFIG=true fi if [ -n "$NINJA" ]; then GENERATOR="Ninja Multi-Config" if ! cmake -E capabilities | grep -F "$GENERATOR" > /dev/null; then SINGLE_CONFIG=true GENERATOR="Ninja" fi fi if [ -n "$SINGLE_CONFIG" ]; then if [ ${#CONFIGURATIONS[@]} -eq 0 ]; then if [ -n "${CONFIGURATION:-}" ]; then CONFIGURATIONS=("$CONFIGURATION") else CONFIGURATIONS=("Debug") fi elif [ ${#CONFIGURATIONS[@]} -ne 1 ]; then # It's simplest just to recursively call the script a few times. RECURSIVE_OPTIONS=() if [ -n "$VERBOSE" ]; then RECURSIVE_OPTIONS+=("-V") fi if [ -n "$SKIP_DOWNLOAD" ]; then RECURSIVE_OPTIONS+=("-d") fi if [ -n "$SKIP_EXTRACT" ]; then RECURSIVE_OPTIONS+=("-e") fi if [ -n "$KEEP" ]; then RECURSIVE_OPTIONS+=("-k") fi if [ -n "$UNITY_BUILD" ]; then RECURSIVE_OPTIONS+=("-u") fi if [ -n "$NMAKE" ]; then RECURSIVE_OPTIONS+=("-n") fi if [ -n "$NINJA" ]; then RECURSIVE_OPTIONS+=("-N") fi if [ -n "$PDBS" ]; then RECURSIVE_OPTIONS+=("-P") fi if [ -n "$TEST_FRAMEWORK" ]; then RECURSIVE_OPTIONS+=("-t") fi RECURSIVE_OPTIONS+=("-v $VS_VERSION") RECURSIVE_OPTIONS+=("-p $PLATFORM") RECURSIVE_OPTIONS+=("-i '$INSTALL_PREFIX'") for config in ${CONFIGURATIONS[@]}; do $0 ${RECURSIVE_OPTIONS[@]} -c $config done wrappedExit 1 fi else if [ ${#CONFIGURATIONS[@]} -ne 0 ]; then echo "Ignoring configurations argument - generator is multi-config" fi CONFIGURATIONS=("Release" "Debug" "RelWithDebInfo") fi for i in ${!CONFIGURATIONS[@]}; do case ${CONFIGURATIONS[$i]} in debug|Debug|DEBUG ) CONFIGURATIONS[$i]=Debug ;; release|Release|RELEASE ) CONFIGURATIONS[$i]=Release ;; relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) CONFIGURATIONS[$i]=RelWithDebInfo ;; esac done if [ -z "$NMAKE" ] && [ -z "$NINJA" ]; then if [ $BITS -eq 64 ]; then add_cmake_opts "-G\"$GENERATOR\" -A x64" else add_cmake_opts "-G\"$GENERATOR\" -A Win32" fi else add_cmake_opts "-G\"$GENERATOR\"" fi if [ -n "$SINGLE_CONFIG" ]; then add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}" fi 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_TAG="2024-11-10" VCPKG_PATH="vcpkg-x64-${VS_VERSION:?}-${VCPKG_TAG:?}" VCPKG_PDB_PATH="vcpkg-x64-${VS_VERSION:?}-pdb-${VCPKG_TAG:?}" VCPKG_MANIFEST="${VCPKG_PATH:?}.txt" VCPKG_PDB_MANIFEST="${VCPKG_PDB_PATH:?}.txt" echo echo "===================================" echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}" echo "===================================" echo mkdir -p deps cd deps DEPS="$(pwd)" if [ -z $SKIP_DOWNLOAD ]; then echo "Downloading dependency packages." echo DEPS_BASE_URL="https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows" download "${VCPKG_MANIFEST:?}" \ "${DEPS_BASE_URL}/${VCPKG_MANIFEST:?}" \ "${VCPKG_MANIFEST:?}" if [ -n "${VCPKG_PDB_MANIFEST:?}" ]; then download "${VCPKG_PDB_PATH:?}" \ "${DEPS_BASE_URL}/${VCPKG_PDB_MANIFEST:?}" \ "${VCPKG_PDB_MANIFEST:?}" fi fi cd .. #/.. # Set up dependencies BUILD_DIR="MSVC${MSVC_DISPLAY_YEAR}_${BITS}" if [ -n "$NMAKE" ]; then BUILD_DIR="${BUILD_DIR}_NMake" elif [ -n "$NINJA" ]; then BUILD_DIR="${BUILD_DIR}_Ninja" fi if [ -n "$SINGLE_CONFIG" ]; then BUILD_DIR="${BUILD_DIR}_${CONFIGURATIONS[0]}" fi if [ -z $KEEP ]; then echo echo "(Re)Creating build directory." rm -rf "$BUILD_DIR" fi mkdir -p "${BUILD_DIR}/deps" cd "${BUILD_DIR}/deps" DEPS_INSTALL="$(pwd)" cd $DEPS echo echo "Extracting dependencies, this might take a while..." echo "---------------------------------------------------" echo cd $DEPS echo printf "vcpkg packages ${VCPKG_TAG:?}... " { if [[ -d "${VCPKG_PATH:?}" ]]; then printf "Exists. " else download_from_manifest "${VCPKG_MANIFEST:?}" eval 7z x -y -o"${VCPKG_PATH:?}" "${MANIFEST_FILE:?}" ${STRIP} fi if [ -n "${PDBS}" ]; then if [[ -d "${VCPKG_PDB_PATH:?}" ]]; then printf "PDB exists. " else download_from_manifest "${VCPKG_PDB_MANIFEST:?}" eval 7z x -y -o"${VCPKG_PDB_PATH:?}" "${MANIFEST_FILE:?}" ${STRIP} fi fi add_cmake_opts -DCMAKE_TOOLCHAIN_FILE="$(real_pwd)/${VCPKG_PATH:?}/scripts/buildsystems/vcpkg.cmake" add_cmake_opts -DLuaJit_INCLUDE_DIR="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/include/luajit" add_cmake_opts -DLuaJit_LIBRARY="$(real_pwd)/${VCPKG_PATH:?}/installed/x64-windows/lib/lua51.lib" for CONFIGURATION in ${CONFIGURATIONS[@]}; do 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 VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/bin" add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Release/MyGUIEngine.dll" fi 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 printf "Qt ${QT_VER}... " { if [ $BITS -eq 64 ]; then SUFFIX="_64" else SUFFIX="" fi cd $DEPS_INSTALL QT_SDK="$(real_pwd)/Qt/${QT_VER}/msvc${QT_MSVC_YEAR}${SUFFIX}" 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 rm -rf Qt mkdir Qt cd Qt run_cmd "${DEPS}/aqt_x64-${AQT_VERSION}.exe" install-qt windows desktop ${QT_VER} "win${BITS}_msvc${QT_MSVC_YEAR}${SUFFIX}" printf " Cleaning up extraneous data... " rm -rf Qt/{aqtinstall.log,Tools} echo Done. fi 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 DLLSUFFIX="d" else DLLSUFFIX="" fi if [ "${QT_MAJOR_VER}" -eq 6 ]; then add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets,Svg}${DLLSUFFIX}.dll # 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 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_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 echo Done. } add_cmake_opts -DCMAKE_PREFIX_PATH="\"${QT_SDK}\"" echo cd $DEPS_INSTALL/.. 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 ) echo " Building subproject: Components." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; openmw ) echo " Building subproject: OpenMW." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENCS=no \ -DBUILD_WIZARD=no ;; opencs ) echo " Building subproject: OpenCS." add_cmake_opts -DBUILD_ESSIMPORTER=no \ -DBUILD_LAUNCHER=no \ -DBUILD_MWINIIMPORTER=no \ -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; misc ) echo " Building subprojects: Misc." add_cmake_opts -DBUILD_OPENCS=no \ -DBUILD_OPENMW=no ;; esac fi # NOTE: Disable this when/if we want to run test cases #if [ -z $CI ]; then for CONFIGURATION in ${CONFIGURATIONS[@]}; do echo "- Copying Runtime DLLs for $CONFIGURATION..." DLL_PREFIX="" if [ -z $SINGLE_CONFIG ]; then mkdir -p $CONFIGURATION DLL_PREFIX="$CONFIGURATION/" fi for DLL in ${RUNTIME_DLLS[$CONFIGURATION]}; do TARGET="$(basename "$DLL")" if [[ "$DLL" == *":"* ]]; then originalIFS="$IFS" IFS=':'; SPLIT=( ${DLL} ); IFS=$originalIFS DLL=${SPLIT[0]} TARGET=${SPLIT[1]} fi echo " ${TARGET}." cp "$DLL" "${DLL_PREFIX}$TARGET" done echo echo "- OSG Plugin DLLs..." mkdir -p ${DLL_PREFIX}osgPlugins-3.6.5 for DLL in ${OSG_PLUGINS[$CONFIGURATION]}; do echo " $(basename $DLL)." cp "$DLL" ${DLL_PREFIX}osgPlugins-3.6.5 done echo echo "- Qt Platform DLLs..." mkdir -p ${DLL_PREFIX}platforms for DLL in ${QT_PLATFORMS[$CONFIGURATION]}; do echo " $(basename $DLL)" cp "$DLL" "${DLL_PREFIX}platforms" done echo echo "- Qt Style DLLs..." mkdir -p ${DLL_PREFIX}styles for DLL in ${QT_STYLES[$CONFIGURATION]}; do echo " $(basename $DLL)" 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 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; } # There are so many arguments now that I'm going to document them: # * products: allow Visual Studio or standalone build tools # * version: obvious. Awk helps make a version range by adding one. # * property installationPath: only give the installation path. # * latest: return only one result if several candidates exist. Prefer the last installed/updated # * requires: make sure it's got the MSVC compiler instead of, for example, just the .NET compiler. The .x86.x64 suffix means it's for either, not that it's the x64 on x86 cross compiler as you always get both MSVC_INSTALLATION_PATH=$(vswhere -products '*' -version "[$MSVC_REAL_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64) if [ -z "$MSVC_INSTALLATION_PATH" ]; then echo "vswhere was unable to find MSVC $MSVC_DISPLAY_YEAR" wrappedExit 1 fi echo "@\"${MSVC_INSTALLATION_PATH}\Common7\Tools\VsDevCmd.bat\" -no_logo -arch=$([ $BITS -eq 64 ] && echo "amd64" || echo "x86") -host_arch=$([ $(uname -m) == 'x86_64' ] && echo "amd64" || echo "x86")" > ActivateMSVC.bat cp "../CI/activate_msvc.sh" . sed -i "s/\$MSVC_DISPLAY_YEAR/$MSVC_DISPLAY_YEAR/g" activate_msvc.sh source ./activate_msvc.sh cp "../CI/ActivateMSVC.ps1" . sed -i "s/\$MSVC_DISPLAY_YEAR/$MSVC_DISPLAY_YEAR/g" ActivateMSVC.ps1 echo "done." echo fi if [ -z $VERBOSE ]; then printf -- "- Configuring... " else echo "- cmake .. $CMAKE_OPTS" fi RET=0 run_cmd cmake .. $CMAKE_OPTS || RET=$? if [ -z $VERBOSE ]; then if [ $RET -eq 0 ]; then echo Done. else echo Failed. fi fi if [ $RET -ne 0 ]; then wrappedExit $RET fi echo "Script completed successfully." echo "You now have an OpenMW build system at $(unixPathAsWindows "$(pwd)")" if [ -n "$ACTIVATE_MSVC" ]; then echo echo "Note: you must manually activate MSVC for the shell in which you want to do the build." echo echo "Some scripts have been created in the build directory to do so in an existing shell." echo "Bash: source activate_msvc.sh" echo "CMD: ActivateMSVC.bat" echo "PowerShell: ActivateMSVC.ps1" echo echo "You may find options to launch a Development/Native Tools/Cross Tools shell in your start menu or Visual Studio." echo if [ $(uname -m) == 'x86_64' ]; then if [ $BITS -eq 64 ]; then inheritEnvironments=msvc_x64_x64 else inheritEnvironments=msvc_x64 fi else if [ $BITS -eq 64 ]; then inheritEnvironments=msvc_x86_x64 else inheritEnvironments=msvc_x86 fi fi echo "In Visual Studio 15.3 (2017 Update 3) or later, try setting '\"inheritEnvironments\": [ \"$inheritEnvironments\" ]' in CMakeSettings.json to build in the IDE." fi wrappedExit $RET openmw-openmw-0.49.0/CI/check_clang_format.sh000077500000000000000000000006171503074453300210470ustar00rootroot00000000000000#!/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 ) openmw-openmw-0.49.0/CI/check_cmake_format.sh000077500000000000000000000003321503074453300210350ustar00rootroot00000000000000#!/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 openmw-openmw-0.49.0/CI/check_file_names.sh000077500000000000000000000004701503074453300205120ustar00rootroot00000000000000#!/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 openmw-openmw-0.49.0/CI/check_package.osx.sh000077500000000000000000000011111503074453300206040ustar00rootroot00000000000000#!/usr/bin/env bash hdiutil attach ./*.dmg -mountpoint "${TRAVIS_BUILD_DIR}/openmw-package" > /dev/null || echo "hdutil has failed" EXPECTED_PACKAGE_FILES=('Applications' 'OpenMW-CS.app' 'OpenMW.app') PACKAGE_FILES=$(ls "${TRAVIS_BUILD_DIR}/openmw-package" | LC_ALL=C sort) DIFF=$(diff <(printf "%s\n" "${EXPECTED_PACKAGE_FILES[@]}") <(printf "%s\n" "${PACKAGE_FILES[@]}")) DIFF_STATUS=$? if [[ $DIFF_STATUS -ne 0 ]]; then echo "The package should only contain an Applications symlink and two applications, see the following diff for details." >&2 echo "$DIFF" >&2 exit 1 fi openmw-openmw-0.49.0/CI/check_qt_translations.sh000077500000000000000000000010321503074453300216300ustar00rootroot00000000000000#!/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) openmw-openmw-0.49.0/CI/file_name_exceptions.txt000066400000000000000000000036261503074453300216430ustar00rootroot00000000000000apps/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 openmw-openmw-0.49.0/CI/github.env000066400000000000000000000000321503074453300167020ustar00rootroot00000000000000VCPKG_DEPS_TAG=2024-11-10 openmw-openmw-0.49.0/CI/install_debian_deps.sh000077500000000000000000000066631503074453300212500ustar00rootroot00000000000000#!/bin/bash set -euo pipefail print_help() { echo "usage: $0 [group]..." echo echo " available groups: "${!GROUPED_DEPS[@]}"" } declare -rA GROUPED_DEPS=( [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]=" libboost-program-options-dev libboost-system-dev libboost-iostreams-dev libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libsdl2-dev libqt5opengl5-dev qttools5-dev qttools5-dev-tools libopenal-dev 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 " # These dependencies can alternatively be built and linked statically. [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. # # * MyGUI and OSG: libsdl2-dev liblz4-dev libfreetype6-dev # * OSG: libgl-dev # # Plugins: # * DAE: libcollada-dom-dev libboost-system-dev libboost-filesystem-dev # * JPEG: libjpeg-dev # * PNG: libpng-dev [openmw-deps-static]=" 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 >&2 print_help exit 1 fi deps=() for group in "$@"; do if [[ ! -v GROUPED_DEPS[$group] ]]; then >&2 echo "error: unknown group ${group}" exit 1 fi deps+=(${GROUPED_DEPS[$group]}) done export APT_CACHE_DIR="${PWD}/apt-cache" export DEBIAN_FRONTEND=noninteractive set -x mkdir -pv "$APT_CACHE_DIR" while true; do apt-get update -yqq && break done apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null while true; do add-apt-repository -y ppa:openmw/openmw && break done while true; do add-apt-repository -y ppa:openmw/openmw-daily && break done while true; do add-apt-repository -y ppa:openmw/staging && break done apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null apt list --installed openmw-openmw-0.49.0/CI/macos/000077500000000000000000000000001503074453300160155ustar00rootroot00000000000000openmw-openmw-0.49.0/CI/macos/before_install.amd64.sh000077500000000000000000000006711503074453300222620ustar00rootroot00000000000000#!/bin/sh -ex arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" arch -x86_64 /usr/local/bin/brew install curl xquartz gd fontconfig freetype harfbuzz brotli s3cmd ccache cmake qt@6 openal-soft icu4c yaml-cpp sqlite curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip unzip -o ~/openmw-deps.zip -d /tmp > /dev/null openmw-openmw-0.49.0/CI/macos/before_install.arm64.sh000077500000000000000000000010251503074453300222720ustar00rootroot00000000000000#!/bin/sh -ex brew tap --repair brew update --quiet brew install curl xquartz gd fontconfig freetype harfbuzz brotli s3cmd command -v ccache >/dev/null 2>&1 || brew install ccache command -v cmake >/dev/null 2>&1 || brew install cmake command -v qmake >/dev/null 2>&1 || brew install qt@6 # Install deps brew install openal-soft icu4c yaml-cpp sqlite curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null openmw-openmw-0.49.0/CI/macos/build.sh000077500000000000000000000002541503074453300174540ustar00rootroot00000000000000#!/bin/sh -ex cd build if [[ "${MACOS_AMD64}" ]]; then arch -x86_64 make -j $(sysctl -n hw.logicalcpu) package else make -j $(sysctl -n hw.logicalcpu) package fi openmw-openmw-0.49.0/CI/macos/ccache_prep.sh000077500000000000000000000002101503074453300206010ustar00rootroot00000000000000#!/bin/sh -ex if [[ "${MACOS_AMD64}" ]]; then arch -x86_64 ccache -z -M "${CCACHE_SIZE}" else ccache -z -M "${CCACHE_SIZE}" fi openmw-openmw-0.49.0/CI/macos/ccache_save.sh000077500000000000000000000001401503074453300205730ustar00rootroot00000000000000#!/bin/sh -ex if [[ "${MACOS_AMD64}" ]]; then arch -x86_64 ccache -s else ccache -s fi openmw-openmw-0.49.0/CI/org.openmw.OpenMW.devel.yaml000066400000000000000000000147741503074453300221510ustar00rootroot00000000000000--- 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" openmw-openmw-0.49.0/CI/run_integration_tests.sh000077500000000000000000000010301503074453300216750ustar00rootroot00000000000000#!/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 --verbose --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 openmw-openmw-0.49.0/CI/teal_ci.sh000077500000000000000000000004741503074453300166570ustar00rootroot00000000000000#!/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 v0.4.0 luarocks make cyan-0.4.0-1.rockspec popd cyan version scripts/generate_teal_declarations.sh ./teal_declarations openmw-openmw-0.49.0/CI/ubuntu_gcc_preprocess.sh000077500000000000000000000044151503074453300216610ustar00rootroot00000000000000#!/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" openmw-openmw-0.49.0/CMakeLists.txt000066400000000000000000001457771503074453300172050ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.16.0) # 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() 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) option(BUILD_WIZARD "Build Installation Wizard" ON) option(BUILD_MWINIIMPORTER "Build MWiniImporter" ON) option(BUILD_OPENCS "Build OpenMW Construction Set" ON) option(BUILD_ESSIMPORTER "Build ESS (Morrowind save game) importer" ON) option(BUILD_BSATOOL "Build BSA extractor" ON) 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_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 (BUILD_LAUNCHER OR BUILD_OPENCS OR BUILD_WIZARD OR BUILD_OPENCS_TESTS) set(USE_QT TRUE) else() set(USE_QT FALSE) endif() # If the user doesn't supply a CMAKE_BUILD_TYPE via command line, choose one for them. IF(NOT CMAKE_BUILD_TYPE) SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build, options are: None(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) Debug Release RelWithDebInfo MinSizeRel." FORCE) set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS None Debug Release RelWithDebInfo MinSizeRel) 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}") endif (APPLE) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) if (ANDROID) set(CMAKE_FIND_ROOT_PATH ${OPENMW_DEPENDENCIES_DIR} "${CMAKE_FIND_ROOT_PATH}") endif() # Version message(STATUS "Configuring OpenMW...") set(OPENMW_VERSION_MAJOR 0) set(OPENMW_VERSION_MINOR 49) set(OPENMW_VERSION_RELEASE 0) set(OPENMW_LUA_API_REVISION 76) set(OPENMW_POSTPROCESSING_API_REVISION 2) set(OPENMW_VERSION_COMMITHASH "") set(OPENMW_VERSION_TAGHASH "") 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/") set(GIT_CHECKOUT FALSE) if(EXISTS ${PROJECT_SOURCE_DIR}/.git) find_package(Git) if(GIT_FOUND) set(GIT_CHECKOUT TRUE) else(GIT_FOUND) message(WARNING "Git executable not found") endif(GIT_FOUND) if(GIT_FOUND) execute_process ( COMMAND ${GIT_EXECUTABLE} log -1 --format='%aI' WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} RESULT_VARIABLE EXITCODE3 OUTPUT_VARIABLE OPENMW_VERSION_COMMITDATE OUTPUT_STRIP_TRAILING_WHITESPACE) if(NOT EXITCODE3) string(SUBSTRING ${OPENMW_VERSION_COMMITDATE} 1 10 OPENMW_VERSION_COMMITDATE) endif(NOT EXITCODE3) endif(GIT_FOUND) endif(EXISTS ${PROJECT_SOURCE_DIR}/.git) # Macros include(OpenMWMacros) include(WholeArchive) # doxygen main page configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp") 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) set(_bullet_static_default OFF) else() set(_bullet_static_default ON) endif() option(BULLET_STATIC "Link static build of Bullet into the binaries" ${_bullet_static_default}) option(OPENMW_USE_SYSTEM_OSG "Use system provided OpenSceneGraph libraries" ON) if(OPENMW_USE_SYSTEM_OSG) set(_osg_static_default OFF) else() set(_osg_static_default ON) endif() option(OSG_STATIC "Link static build of OpenSceneGraph into the binaries" ${_osg_static_default}) option(OPENMW_USE_SYSTEM_MYGUI "Use system provided mygui library" ON) if(OPENMW_USE_SYSTEM_MYGUI) set(_mygui_static_default OFF) else() set(_mygui_static_default ON) endif() option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_static_default}) 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 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) # 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(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_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.") IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/usr") SET(GLOBAL_CONFIG_PATH "/etc/" CACHE PATH "Set config dir prefix") ELSE() SET(GLOBAL_CONFIG_PATH "${CMAKE_INSTALL_PREFIX}/etc/" CACHE PATH "Set config dir prefix") ENDIF() SET(SYSCONFDIR "${GLOBAL_CONFIG_PATH}/openmw" CACHE PATH "Set config dir") set(OPENMW_RESOURCE_FILES "${DATADIR}/resources" CACHE PATH "location of OpenMW resources files") else() set(OPENMW_RESOURCE_FILES "resources" CACHE PATH "location of OpenMW resources files") endif(APPLE) if (WIN32) 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 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(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 osgGA osgFX osgParticle osgText osgUtil osgShadow osgSim osgViewer ) set(USED_OSG_PLUGINS osgdb_bmp osgdb_dae osgdb_dds osgdb_freetype osgdb_jpeg osgdb_osg osgdb_png 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 # Require at least ffmpeg 3.2 for now SET(FFVER_OK FALSE) find_package(FFmpeg REQUIRED COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMPLE) if(FFmpeg_FOUND) SET(FFVER_OK TRUE) # 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) endif() if(FFmpeg_AVCODEC_VERSION VERSION_LESS "57.64.100") message(STATUS "libavcodec is too old! (${FFmpeg_AVCODEC_VERSION}, wanted 57.64.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_AVUTIL_VERSION VERSION_LESS "55.34.100") message(STATUS "libavutil is too old! (${FFmpeg_AVUTIL_VERSION}, wanted 55.34.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_SWSCALE_VERSION VERSION_LESS "4.2.100") message(STATUS "libswscale is too old! (${FFmpeg_SWSCALE_VERSION}, wanted 4.2.100)") set(FFVER_OK FALSE) endif() if(FFmpeg_SWRESAMPLE_VERSION VERSION_LESS "2.3.100") message(STATUS "libswresample is too old! (${FFmpeg_SWRESAMPLE_VERSION}, wanted 2.3.100)") set(FFVER_OK FALSE) endif() endif() if(NOT FFVER_OK AND NOT APPLE) # unable to detect on version on MacOS < 11.0 message(FATAL_ERROR "FFmpeg version is too old, 3.2 is required" ) endif() endif() if(NOT FFmpeg_FOUND) message(FATAL_ERROR "FFmpeg was not found" ) endif() if(WIN32) message("Can not detect FFmpeg version, at least the 3.2 is required" ) endif() # Required for building the FFmpeg headers add_definitions(-D__STDC_CONSTANT_MACROS) # Required for unity build add_definitions(-DMYGUI_DONT_REPLACE_NULLPTR) # TinyXML option(USE_SYSTEM_TINYXML "Use system TinyXML library instead of internal." OFF) if (USE_SYSTEM_TINYXML) find_package(TinyXML REQUIRED) add_definitions (-DTIXML_USE_STL) include_directories(SYSTEM ${TinyXML_INCLUDE_DIRS}) endif() # Platform specific if (WIN32) # Suppress WinMain(), provided by SDL add_definitions(-DSDL_MAIN_HANDLED) # Get rid of useless crud from windows.h 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) set(REQUIRED_BULLET_VERSION 286) # Bullet 286 required due to runtime bugfixes for btCapsuleShape if (DEFINED ENV{TRAVIS_BRANCH} OR DEFINED ENV{APPVEYOR}) set(REQUIRED_BULLET_VERSION 283) # but for build testing, 283 is fine endif() # First, try BulletConfig-float64.cmake which comes with Debian derivatives. # This file does not define the Bullet version in a CMake-friendly way. find_package(Bullet CONFIGS BulletConfig-float64.cmake QUIET COMPONENTS BulletCollision LinearMath) if (BULLET_FOUND) string(REPLACE "." "" _bullet_version_num ${BULLET_VERSION_STRING}) if (_bullet_version_num VERSION_LESS REQUIRED_BULLET_VERSION) message(FATAL_ERROR "System bullet version too old, OpenMW requires at least ${REQUIRED_BULLET_VERSION}, got ${_bullet_version_num}") endif() # Fix the relative include: set(BULLET_INCLUDE_DIRS "${BULLET_ROOT_DIR}/${BULLET_INCLUDE_DIRS}") include(FindPackageMessage) find_package_message(Bullet "Found Bullet: ${BULLET_LIBRARIES} ${BULLET_VERSION_STRING}" "${BULLET_VERSION_STRING}-float64") else() find_package(Bullet ${REQUIRED_BULLET_VERSION} REQUIRED COMPONENTS BulletCollision LinearMath) endif() # Only link the Bullet libraries that we need: string(REGEX MATCHALL "((optimized|debug);)?[^;]*(BulletCollision|LinearMath)[^;]*" BULLET_LIBRARIES "${BULLET_LIBRARIES}") include(cmake/CheckBulletPrecision.cmake) if (HAS_DOUBLE_PRECISION_BULLET) message(STATUS "Bullet uses double precision") else() message(FATAL_ERROR "Bullet does not uses double precision") endif() endif() if (NOT WIN32 AND BUILD_WIZARD) # windows users can just run the morrowind installer find_package(LIBUNSHIELD REQUIRED) # required only for non win32 when building openmw-wizard set(OPENMW_USE_UNSHIELD TRUE) endif() # Fix for not visible pthreads functions for linker with glibc 2.15 if (UNIX AND NOT APPLE) find_package (Threads) endif() # Look for stdint.h include(CheckIncludeFile) check_include_file(stdint.h HAVE_STDINT_H) if(NOT HAVE_STDINT_H) unset(HAVE_STDINT_H CACHE) message(FATAL_ERROR "stdint.h was not found" ) endif() if(OPENMW_USE_SYSTEM_OSG) 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}) endif() endif() include_directories(BEFORE SYSTEM ${OPENSCENEGRAPH_INCLUDE_DIRS}) if(OSG_STATIC) add_definitions(-DOSG_LIBRARY_STATIC) endif() include(cmake/CheckOsgMultiview.cmake) if(HAVE_MULTIVIEW) add_definitions(-DOSG_HAS_MULTIVIEW) endif(HAVE_MULTIVIEW) set(BOOST_COMPONENTS iostreams program_options system) find_package(Boost 1.70.0 CONFIG REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS}) if(OPENMW_USE_SYSTEM_MYGUI) find_package(MyGUI 3.4.3 REQUIRED) endif() find_package(SDL2 2.0.10 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 "." ${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(${COLLADA_DOM_LIBRARY_DIRS}) if(MYGUI_STATIC) add_definitions(-DMYGUI_STATIC) endif (MYGUI_STATIC) if (APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw-Info.plist.in "${APP_BUNDLE_DIR}/Contents/Info.plist") configure_file(${OpenMW_SOURCE_DIR}/files/mac/openmw.icns "${APP_BUNDLE_DIR}/Contents/Resources/OpenMW.icns" COPYONLY) endif (APPLE) 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/) # Specify build paths if (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${APP_BUNDLE_DIR}/Contents/MacOS") if (OPENMW_OSX_DEPLOYMENT) SET(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) endif() else (APPLE) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}") endif (APPLE) # Other files pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg "${OpenMW_BINARY_DIR}" "defaults.bin") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}" "openmw.appdata.xml") if (APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg "${OpenMW_BINARY_DIR}/openmw.cfg") elseif (WIN32) configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local "${OpenMW_BINARY_DIR}" "openmw.cfg") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local "${OpenMW_BINARY_DIR}" "openmw.cfg.install") else () configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg.local "${OpenMW_BINARY_DIR}" "openmw.cfg") configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg "${OpenMW_BINARY_DIR}" "openmw.cfg.install") endif () 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 "${OpenMW_BINARY_DIR}" "resources/defaultfilters") configure_resource_file(${OpenMW_SOURCE_DIR}/files/gamecontrollerdb.txt "${OpenMW_BINARY_DIR}" "gamecontrollerdb.txt") if (NOT WIN32 AND NOT APPLE) configure_file(${OpenMW_SOURCE_DIR}/files/org.openmw.launcher.desktop "${OpenMW_BINARY_DIR}/org.openmw.launcher.desktop") configure_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml "${OpenMW_BINARY_DIR}/openmw.appdata.xml") configure_file(${OpenMW_SOURCE_DIR}/files/org.openmw.cs.desktop "${OpenMW_BINARY_DIR}/org.openmw.cs.desktop") endif() if(OPENMW_LTO_BUILD) 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() endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(OPENMW_CXX_FLAGS "-Wall -Wextra -Wundef -Wextra-semi -Wno-unused-parameter -pedantic -Wno-long-long -Wnon-virtual-dtor -Wunused ${OPENMW_CXX_FLAGS}") if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) # https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105438 set(OPENMW_CXX_FLAGS "-Wno-array-bounds ${OPENMW_CXX_FLAGS}") endif() if (APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -stdlib=libc++") endif() if (CMAKE_CXX_COMPILER_ID STREQUAL Clang AND NOT APPLE) if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 3.6 OR CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 3.6) set(OPENMW_CXX_FLAGS "${OPENMW_CXX_FLAGS} -Wno-potentially-evaluated-expression") endif () endif() if (CMAKE_CXX_COMPILER_ID STREQUAL GNU) set(OPENMW_CXX_FLAGS "${OPENMW_CXX_FLAGS} -Wno-unused-but-set-parameter -Wduplicated-branches -Wduplicated-cond -Wlogical-op") endif() endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) # Extern add_subdirectory (extern/osg-ffmpeg-videoplayer) add_subdirectory (extern/oics) add_subdirectory (extern/Base64) 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) # Apps and tools if (BUILD_OPENMW OR BUILD_OPENMW_TESTS) add_subdirectory( apps/openmw ) endif() if (BUILD_BSATOOL) add_subdirectory( apps/bsatool ) endif() if (BUILD_ESMTOOL) add_subdirectory( apps/esmtool ) endif() if (BUILD_LAUNCHER) add_subdirectory( apps/launcher ) endif() if (BUILD_MWINIIMPORTER) add_subdirectory( apps/mwiniimporter ) endif() if (BUILD_ESSIMPORTER) add_subdirectory (apps/essimporter ) endif() if (BUILD_OPENCS OR BUILD_OPENCS_TESTS) add_subdirectory (apps/opencs) endif() if (BUILD_WIZARD) add_subdirectory(apps/wizard) endif() if (BUILD_NIFTEST) add_subdirectory(apps/niftest) endif() if (BUILD_COMPONENTS_TESTS) add_subdirectory(apps/components_tests) endif() if (BUILD_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) 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() 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 set(WARNINGS "/W4") set(WARNINGS_DISABLE 4100 # Unreferenced formal parameter (-Wunused-parameter) 4127 # Conditional expression is constant 4996 # Function was declared deprecated 5054 # Deprecated operations between enumerations of different types caused by Qt headers ) foreach(d ${WARNINGS_DISABLE}) list(APPEND WARNINGS "/wd${d}") endforeach(d) if(OPENMW_MSVC_WERROR) list(APPEND WARNINGS "/WX") endif() target_compile_options(components PRIVATE ${WARNINGS}) target_compile_options(osg-ffmpeg-videoplayer PRIVATE ${WARNINGS}) if (MSVC_VERSION GREATER_EQUAL 1915 AND MSVC_VERSION LESS 1920) target_compile_definitions(components INTERFACE _ENABLE_EXTENDED_ALIGNED_STORAGE) endif() if (BUILD_BSATOOL) target_compile_options(bsatool PRIVATE ${WARNINGS}) endif() if (BUILD_ESMTOOL) target_compile_options(esmtool PRIVATE ${WARNINGS}) endif() if (BUILD_ESSIMPORTER) target_compile_options(openmw-essimporter PRIVATE ${WARNINGS}) endif() if (BUILD_LAUNCHER) target_compile_options(openmw-launcher PRIVATE ${WARNINGS}) endif() if (BUILD_MWINIIMPORTER) target_compile_options(openmw-iniimporter PRIVATE ${WARNINGS}) endif() if (BUILD_OPENCS) target_compile_options(openmw-cs PRIVATE ${WARNINGS}) endif() if (BUILD_OPENMW) target_compile_options(openmw PRIVATE ${WARNINGS}) endif() if (BUILD_WIZARD) target_compile_options(openmw-wizard PRIVATE ${WARNINGS}) endif() if (BUILD_COMPONENTS_TESTS) target_compile_options(components-tests PRIVATE ${WARNINGS}) endif() if (BUILD_BENCHMARKS) target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ${WARNINGS}) endif() if (BUILD_NAVMESHTOOL) target_compile_options(openmw-navmeshtool PRIVATE ${WARNINGS}) endif() if (BUILD_BULLETOBJECTTOOL) target_compile_options(openmw-bulletobjecttool PRIVATE ${WARNINGS} ${MT_BUILD}) endif() if (BUILD_OPENCS_TESTS) target_compile_options(openmw-cs-tests PRIVATE ${WARNINGS}) endif() if (BUILD_OPENMW_TESTS) target_compile_options(openmw-tests PRIVATE ${WARNINGS}) endif() endif(MSVC) # TODO: At some point release builds should not use the console but rather write to a log file #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_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 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("${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) set(CPACK_GENERATOR "DragNDrop") set(CPACK_PACKAGE_VERSION ${OPENMW_VERSION}) set(CPACK_PACKAGE_VERSION_MAJOR ${OPENMW_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${OPENMW_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${OPENMW_VERSION_RELEASE}) set(INSTALLED_OPENMW_APP "\${CMAKE_INSTALL_PREFIX}/${APP_BUNDLE_NAME}") set(INSTALLED_OPENCS_APP "\${CMAKE_INSTALL_PREFIX}/${OPENCS_BUNDLE_NAME}") install(CODE " set(BU_CHMOD_BUNDLE_ITEMS ON) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) include(BundleUtilities) " COMPONENT Runtime) set(ABSOLUTE_PLUGINS "") set(OSGPlugins_DONT_FIND_DEPENDENCIES 1) find_package(OSGPlugins REQUIRED COMPONENTS ${USED_OSG_PLUGINS}) foreach (PLUGIN_NAME ${USED_OSG_PLUGINS}) string(TOUPPER ${PLUGIN_NAME} PLUGIN_NAME_UC) if(${PLUGIN_NAME_UC}_LIBRARY_RELEASE) set(PLUGIN_ABS ${${PLUGIN_NAME_UC}_LIBRARY_RELEASE}) elseif(${PLUGIN_NAME_UC}_LIBRARY) set(PLUGIN_ABS ${${PLUGIN_NAME_UC}_LIBRARY}) else() message(FATAL_ERROR "Can't find library file for ${PLUGIN_NAME}") # We used to construct the path manually from OSGPlugins_LIB_DIR and the plugin name. # Maybe that could be restored as a fallback? endif() set(ABSOLUTE_PLUGINS ${PLUGIN_ABS} ${ABSOLUTE_PLUGINS}) endforeach () set(OSG_PLUGIN_PREFIX_DIR "osgPlugins-${OPENSCENEGRAPH_VERSION}") # installs used plugins in bundle at given path (bundle_path must be relative to ${CMAKE_INSTALL_PREFIX}) # and returns list of install paths for all installed plugins function (install_plugins_for_bundle bundle_path plugins_var) set(RELATIVE_PLUGIN_INSTALL_BASE "${bundle_path}/Contents/PlugIns/${OSG_PLUGIN_PREFIX_DIR}") set(PLUGINS "") set(PLUGIN_INSTALL_BASE "\${CMAKE_INSTALL_PREFIX}/${RELATIVE_PLUGIN_INSTALL_BASE}") foreach (PLUGIN ${ABSOLUTE_PLUGINS}) get_filename_component(PLUGIN_RELATIVE ${PLUGIN} NAME) get_filename_component(PLUGIN_RELATIVE_WE ${PLUGIN} NAME_WE) set(PLUGIN_DYLIB_IN_BUNDLE "${PLUGIN_INSTALL_BASE}/${PLUGIN_RELATIVE}") set(PLUGINS ${PLUGINS} "${PLUGIN_DYLIB_IN_BUNDLE}") install(CODE " copy_resolved_item_into_bundle(\"${PLUGIN}\" \"${PLUGIN_DYLIB_IN_BUNDLE}\") " COMPONENT Runtime) endforeach () set(${plugins_var} ${PLUGINS} PARENT_SCOPE) endfunction (install_plugins_for_bundle) 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) if (\${item} MATCHES ${OSG_PLUGIN_PREFIX_DIR}) set(path \"@executable_path/../PlugIns/${OSG_PLUGIN_PREFIX_DIR}\") set(\${default_embedded_path_var} \"\${path}\" PARENT_SCOPE) endif() endfunction() 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) if (multi_config) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}/$") else () SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () if(WIN32) INSTALL(DIRECTORY "${INSTALL_SOURCE}/" DESTINATION "." FILES_MATCHING PATTERN "*.dll" PATTERN ".cmake" EXCLUDE PATTERN ".qt" EXCLUDE PATTERN "_CPack_Packages" EXCLUDE PATTERN "_deps" EXCLUDE PATTERN "apps" EXCLUDE PATTERN "bin" EXCLUDE PATTERN "CMakeFiles" EXCLUDE PATTERN "components" EXCLUDE PATTERN "deps" EXCLUDE PATTERN "docs" EXCLUDE PATTERN "extern" EXCLUDE PATTERN "files" EXCLUDE PATTERN "lib" EXCLUDE PATTERN "SymStore" EXCLUDE PATTERN "symstore-venv" EXCLUDE PATTERN "Testing" EXCLUDE PATTERN "tests_output" EXCLUDE PATTERN "try-compile") INSTALL(DIRECTORY "${INSTALL_SOURCE}/" DESTINATION "." CONFIGURATIONS Debug;RelWithDebInfo FILES_MATCHING PATTERN "*.pdb" PATTERN ".cmake" EXCLUDE PATTERN ".qt" EXCLUDE PATTERN "_CPack_Packages" EXCLUDE PATTERN "_deps" EXCLUDE PATTERN "apps" EXCLUDE PATTERN "bin" EXCLUDE PATTERN "CMakeFiles" EXCLUDE PATTERN "components" EXCLUDE PATTERN "deps" EXCLUDE PATTERN "docs" EXCLUDE PATTERN "extern" EXCLUDE PATTERN "files" EXCLUDE PATTERN "lib" EXCLUDE PATTERN "SymStore" EXCLUDE PATTERN "symstore-venv" EXCLUDE PATTERN "Testing" EXCLUDE PATTERN "tests_output" EXCLUDE PATTERN "try-compile") INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "." RENAME "openmw.cfg") 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}/AUTHORS.md" DESTINATION "." RENAME "AUTHORS.txt") INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".") INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".") INSTALL(DIRECTORY "${INSTALL_SOURCE}/resources" DESTINATION ".") SET(CPACK_GENERATOR "NSIS") SET(CPACK_PACKAGE_NAME "OpenMW") SET(CPACK_PACKAGE_VENDOR "OpenMW.org") SET(CPACK_PACKAGE_VERSION ${OPENMW_VERSION}) SET(CPACK_PACKAGE_VERSION_MAJOR ${OPENMW_VERSION_MAJOR}) SET(CPACK_PACKAGE_VERSION_MINOR ${OPENMW_VERSION_MINOR}) SET(CPACK_PACKAGE_VERSION_PATCH ${OPENMW_VERSION_RELEASE}) SET(CPACK_PACKAGE_EXECUTABLES "openmw;OpenMW") IF(BUILD_LAUNCHER) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-launcher;OpenMW Launcher") ENDIF(BUILD_LAUNCHER) IF(BUILD_OPENCS) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-cs;OpenMW Construction Set") ENDIF(BUILD_OPENCS) IF(BUILD_WIZARD) SET(CPACK_PACKAGE_EXECUTABLES "${CPACK_PACKAGE_EXECUTABLES};openmw-wizard;OpenMW Wizard") ENDIF(BUILD_WIZARD) SET(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '\$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Readme.lnk' '\$INSTDIR\\\\README.txt'") SET(CPACK_NSIS_DELETE_ICONS_EXTRA " !insertmacro MUI_STARTMENU_GETFOLDER Application $MUI_TEMP Delete \\\"$SMPROGRAMS\\\\$MUI_TEMP\\\\Readme.lnk\\\" ") SET(CPACK_RESOURCE_FILE_README "${OpenMW_SOURCE_DIR}/README.md") SET(CPACK_PACKAGE_DESCRIPTION_FILE "${OpenMW_SOURCE_DIR}/README.md") SET(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") SET(CPACK_NSIS_DISPLAY_NAME "OpenMW ${OPENMW_VERSION}") SET(CPACK_NSIS_HELP_LINK "https:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_URL_INFO_ABOUT "https:\\\\\\\\www.openmw.org") SET(CPACK_NSIS_INSTALLED_ICON_NAME "openmw-launcher.exe") SET(CPACK_NSIS_MUI_FINISHPAGE_RUN "openmw-launcher.exe") SET(CPACK_NSIS_MUI_ICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") SET(CPACK_NSIS_MUI_UNIICON "${OpenMW_SOURCE_DIR}/files/windows/openmw.ico") SET(CPACK_PACKAGE_ICON "${OpenMW_SOURCE_DIR}\\\\files\\\\openmw.bmp") SET(VCREDIST64 "${OpenMW_BINARY_DIR}/vcredist_x64.exe" CACHE FILEPATH "Path to vcredist_x64.exe") if(EXISTS ${VCREDIST64}) INSTALL(FILES ${VCREDIST64} DESTINATION "redist") get_filename_component(REDIST_FILENAME "${VCREDIST64}" NAME) SET(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " ExecWait '\\\"$INSTDIR\\\\redist\\\\${REDIST_FILENAME}\\\" /q /norestart' RMDir /r \\\"$INSTDIR\\\\redist\\\" ") endif(EXISTS ${VCREDIST64}) if(CMAKE_CL_64) SET(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64") endif() include(CPack) else(WIN32) # Linux installation # Install binaries IF(BUILD_OPENMW) INSTALL(PROGRAMS "$" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENMW) IF(BUILD_LAUNCHER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-launcher" DESTINATION "${BINDIR}" ) ENDIF(BUILD_LAUNCHER) IF(BUILD_BSATOOL) INSTALL(PROGRAMS "${INSTALL_SOURCE}/bsatool" DESTINATION "${BINDIR}" ) ENDIF(BUILD_BSATOOL) IF(BUILD_ESMTOOL) INSTALL(PROGRAMS "${INSTALL_SOURCE}/esmtool" DESTINATION "${BINDIR}" ) ENDIF(BUILD_ESMTOOL) IF(BUILD_NIFTEST) INSTALL(PROGRAMS "${INSTALL_SOURCE}/niftest" DESTINATION "${BINDIR}" ) ENDIF(BUILD_NIFTEST) IF(BUILD_MWINIIMPORTER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-iniimporter" DESTINATION "${BINDIR}" ) ENDIF(BUILD_MWINIIMPORTER) IF(BUILD_ESSIMPORTER) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-essimporter" DESTINATION "${BINDIR}" ) ENDIF(BUILD_ESSIMPORTER) IF(BUILD_OPENCS) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-cs" DESTINATION "${BINDIR}" ) ENDIF(BUILD_OPENCS) IF(BUILD_WIZARD) INSTALL(PROGRAMS "${INSTALL_SOURCE}/openmw-wizard" DESTINATION "${BINDIR}" ) ENDIF(BUILD_WIZARD) 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") INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/launcher/images/openmw.png" DESTINATION "${ICONDIR}" COMPONENT "openmw") INSTALL(FILES "${OpenMW_BINARY_DIR}/openmw.appdata.xml" DESTINATION "${DATAROOTDIR}/metainfo" COMPONENT "openmw") IF(BUILD_OPENCS) INSTALL(FILES "${OpenMW_BINARY_DIR}/org.openmw.cs.desktop" DESTINATION "${DATAROOTDIR}/applications" COMPONENT "opencs") INSTALL(FILES "${OpenMW_SOURCE_DIR}/files/opencs/openmw-cs.png" DESTINATION "${ICONDIR}" COMPONENT "opencs") ENDIF(BUILD_OPENCS) # 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}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw") IF(BUILD_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") endif(WIN32) endif(OPENMW_OSX_DEPLOYMENT AND APPLE) # 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") endif () configure_file(${OpenMW_SOURCE_DIR}/docs/Doxyfile.cmake ${OpenMW_BINARY_DIR}/docs/Doxyfile @ONLY) configure_file(${OpenMW_SOURCE_DIR}/docs/DoxyfilePages.cmake ${OpenMW_BINARY_DIR}/docs/DoxyfilePages @ONLY) add_custom_target(doc ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/Doxyfile WORKING_DIRECTORY ${OpenMW_BINARY_DIR} COMMENT "Generating Doxygen documentation at ${OpenMW_BINARY_DIR}/docs/Doxygen" VERBATIM) add_custom_target(doc_pages ${DOXYGEN_EXECUTABLE} ${OpenMW_BINARY_DIR}/docs/DoxyfilePages 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() openmw-openmw-0.49.0/CONTRIBUTING.md000066400000000000000000000252131503074453300166540ustar00rootroot00000000000000How to contribute to OpenMW ======================= Not sure what to do with all your free time? Pick out a task from here: 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: * 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 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. Merge request guidelines ======================= To facilitate the review process, your merge request description should include the following, if applicable: * 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 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 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. Unfortunately, the definition of what is a "bug" is not so clear. Consider that your "bug" is actually a feature unless proven otherwise: * We have no way of knowing what the original developers really intended (short of asking them, good luck with that). * What may seem like an illogical mechanic can actually be part of an attempt to balance the game. * Many people will actually like these "bugs" because that is what they remember the game for. * Exploits may be part of the fun of an open-world game - they reward knowledge with power. There are too many of them to plug them all, anyway. OpenMW, in its default configuration, is meant to be a faithful reimplementation of Morrowind, minus things like crash bugs, stability issues and severe design errors. However, we try to avoid touching anything that affects the core gameplay, the balancing of the game or introduces incompatibilities with existing mod content. 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 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 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. * 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 merge requests ======================= 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 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 **merge request guidelines** (first half of this document) were followed. 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 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) 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 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 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". 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 ======================== The master branch should always be in a working state that is not worse than the previous release in any way. If a regression is found, the first and foremost priority should be to get the regression fixed quickly, either by reverting the change that caused it or finding a better solution. Please avoid leaving the project in the 'broken' state for an extensive period of time while proper solutions are found. If the solution takes more than a day or so then it is usually better to revert the offending change first and reapply it later when fixed. Other resources =============== [GitHub blog - how to write the perfect pull request](https://blog.github.com/2015-01-21-how-to-write-the-perfect-pull-request/) openmw-openmw-0.49.0/LICENSE000066400000000000000000001045111503074453300154270ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . openmw-openmw-0.49.0/README.md000066400000000000000000000161131503074453300157010ustar00rootroot00000000000000OpenMW ====== 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.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/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/?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. Getting Started --------------- * [Official forums](https://forum.openmw.org/) * [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) * [Report a bug](https://gitlab.com/OpenMW/openmw/issues) - read the [guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) before submitting your first bug! * [Known issues](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=Bug) The data path ------------- The data path tells OpenMW where to find your Morrowind files. If you run the launcher, OpenMW should be able to pick up the location of these files on its own, if both Morrowind and OpenMW are installed properly (installing Morrowind under WINE is considered a proper install). Command line options -------------------- Syntax: openmw Allowed options: --help print help message --version print version information and quit --data arg (=data) set data directories (later directories have higher priority) --data-local arg set local data directory (highest priority) --fallback-archive arg (=fallback-archive) set fallback BSA archives (later archives have higher priority) --resources arg (=resources) set resources directory --start arg set initial cell --content arg content file(s): esm/esp, or omwgame/omwaddon --no-sound [=arg(=1)] (=0) disable all sounds --script-verbose [=arg(=1)] (=0) verbose script output --script-all [=arg(=1)] (=0) compile all scripts (excluding dialogue scripts) at startup --script-all-dialogue [=arg(=1)] (=0) compile all dialogue scripts at startup --script-console [=arg(=1)] (=0) enable console-only script functionality --script-run arg select a file containing a list of console commands that is executed on startup --script-warn [=arg(=1)] (=1) handling of warnings when compiling scripts 0 - ignore warning 1 - show warning but consider script as correctly compiled anyway 2 - treat warnings as errors --load-savegame arg load a save game file on game startup (specify an absolute filename or a filename relative to the current working directory) --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) --encoding arg (=win1252) Character encoding used in OpenMW game messages: win1250 - Central and Eastern European such as Polish, Czech, Slovak, Hungarian, Slovene, Bosnian, Croatian, Serbian (Latin script), Romanian and Albanian languages win1251 - Cyrillic alphabet such as Russian, Bulgarian, Serbian Cyrillic and other languages win1252 - Western European (Latin) alphabet, used by default --fallback arg fallback values --no-grab Don't grab mouse cursor --export-fonts [=arg(=1)] (=0) Export Morrowind .fnt fonts to PNG image and XML file in current directory --activate-dist arg (=-1) activation distance override --random-seed arg (=) seed value for random number generator openmw-openmw-0.49.0/apps/000077500000000000000000000000001503074453300153635ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/benchmarks/000077500000000000000000000000001503074453300175005ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/benchmarks/CMakeLists.txt000066400000000000000000000002411503074453300222350ustar00rootroot00000000000000if(OPENMW_USE_SYSTEM_BENCHMARK) find_package(benchmark REQUIRED) endif() add_subdirectory(detournavigator) add_subdirectory(esm) add_subdirectory(settings) openmw-openmw-0.49.0/apps/benchmarks/detournavigator/000077500000000000000000000000001503074453300227155ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/benchmarks/detournavigator/CMakeLists.txt000066400000000000000000000013111503074453300254510ustar00rootroot00000000000000openmw_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() openmw-openmw-0.49.0/apps/benchmarks/detournavigator/navmeshtilescache.cpp000066400000000000000000000230511503074453300271100ustar00rootroot00000000000000#include #include #include #include #include #include #include namespace { using namespace DetourNavigator; struct Key { AgentBounds mAgentBounds; TilePosition mTilePosition; RecastMesh mRecastMesh; }; struct Item { Key mKey; PreparedNavMeshData mValue; }; osg::Vec2i generateVec2i(int max, auto& random) { std::uniform_int_distribution distribution(0, max); return osg::Vec2i(distribution(random), distribution(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)); } 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); }); } 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); }); } AreaType toAreaType(int index) { 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; } return AreaType_null; } AreaType generateAreaType(auto& random) { std::uniform_int_distribution distribution(0, 4); return toAreaType(distribution(random)); } void generateAreaTypes(std::output_iterator auto out, std::size_t triangles, auto& random) { std::generate_n(out, triangles, [&] { return generateAreaType(random); }); } void generateWater(std::output_iterator auto out, std::size_t count, auto& random) { std::uniform_real_distribution distribution(0.0, 1.0); std::generate_n(out, count, [&] { return CellWater{ generateVec2i(1000, random), Water{ ESM::Land::REAL_SIZE, distribution(random) } }; }); } Mesh generateMesh(std::size_t triangles, auto& 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; } 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 = 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 = 239; void generateKeys(std::output_iterator auto out, std::size_t count, auto& random) { std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); }); } 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.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique()); *out++ = std::move(key); const std::size_t newSize = cache.getStats().mNavMeshCacheSize; if (size >= newSize) break; size = newSize; } } template void getFromFilledCache(benchmark::State& state) { NavMeshTilesCache cache(maxCacheSize); std::minstd_rand random; std::vector keys; fillCache(std::back_inserter(keys), random, cache); generateKeys(std::back_inserter(keys), keys.size() * (100 - hitPercentage) / 100, random); std::size_t n = 0; for ([[maybe_unused]] auto _ : state) { const auto& key = keys[n++ % keys.size()]; auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh); benchmark::DoNotOptimize(result); } } 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) { NavMeshTilesCache cache(maxCacheSize); std::minstd_rand random; std::vector keys; fillCache(std::back_inserter(keys), random, cache); generateKeys(std::back_inserter(keys), keys.size() * 2, random); std::reverse(keys.begin(), keys.end()); std::size_t n = 0; while (state.KeepRunning()) { const auto& key = keys[n++ % keys.size()]; auto result = cache.set( key.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique()); benchmark::DoNotOptimize(result); } } 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); BENCHMARK(getFromFilledCache_4m_100hit); BENCHMARK(getFromFilledCache_16m_100hit); BENCHMARK(getFromFilledCache_64m_100hit); BENCHMARK(getFromFilledCache_1m_70hit); BENCHMARK(getFromFilledCache_4m_70hit); BENCHMARK(getFromFilledCache_16m_70hit); BENCHMARK(getFromFilledCache_64m_70hit); BENCHMARK(setToBoundedNonEmptyCache_1m); BENCHMARK(setToBoundedNonEmptyCache_4m); BENCHMARK(setToBoundedNonEmptyCache_16m); BENCHMARK(setToBoundedNonEmptyCache_64m); BENCHMARK_MAIN(); openmw-openmw-0.49.0/apps/benchmarks/esm/000077500000000000000000000000001503074453300202645ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/benchmarks/esm/CMakeLists.txt000066400000000000000000000010621503074453300230230ustar00rootroot00000000000000openmw_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() openmw-openmw-0.49.0/apps/benchmarks/esm/benchrefid.cpp000066400000000000000000000212141503074453300230610ustar00rootroot00000000000000#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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] 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(); openmw-openmw-0.49.0/apps/benchmarks/settings/000077500000000000000000000000001503074453300213405ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/benchmarks/settings/CMakeLists.txt000066400000000000000000000013201503074453300240740ustar00rootroot00000000000000openmw_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() openmw-openmw-0.49.0/apps/benchmarks/settings/access.cpp000066400000000000000000000127041503074453300233110ustar00rootroot00000000000000#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 ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::Manager::getFloat("sky blending start", "Fog")); } } void settingsManager2(benchmark::State& state) { for ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] auto _ : state) { static float v = Settings::Manager::getFloat("sky blending start", "Fog"); benchmark::DoNotOptimize(v); } } void localStatic2(benchmark::State& state) { for ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] auto _ : state) { float v = Settings::fog().mSkyBlendingStart.get(); benchmark::DoNotOptimize(v); } } void settingsStorage2(benchmark::State& state) { for ([[maybe_unused]] 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 ([[maybe_unused]] 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 ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::get("Fog", "sky blending start")); } } void settingsStorageGet2(benchmark::State& state) { for ([[maybe_unused]] auto _ : state) { benchmark::DoNotOptimize(Settings::get("Post Processing", "transparent postpass")); benchmark::DoNotOptimize(Settings::get("Camera", "near clip")); } } void settingsStorageGet3(benchmark::State& state) { for ([[maybe_unused]] 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; } openmw-openmw-0.49.0/apps/bsatool/000077500000000000000000000000001503074453300170265ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/bsatool/CMakeLists.txt000066400000000000000000000011071503074453300215650ustar00rootroot00000000000000set(BSATOOL bsatool.cpp ) source_group(apps\\bsatool FILES ${BSATOOL}) # Main executable openmw_add_executable(bsatool ${BSATOOL} ) target_link_libraries(bsatool Boost::program_options components ) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(bsatool PRIVATE --coverage) target_link_libraries(bsatool gcov) endif() if (WIN32) install(TARGETS bsatool RUNTIME DESTINATION ".") endif() if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(bsatool PRIVATE ) endif() openmw-openmw-0.49.0/apps/bsatool/bsatool.cpp000066400000000000000000000261501503074453300212010ustar00rootroot00000000000000#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; struct Arguments { std::string mode; 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) { 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"); 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); // 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(); bpo::store(valid_opts, variables); } catch (std::exception& e) { std::cout << "ERROR parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } bpo::notify(variables); if (variables.count("help")) { std::cout << desc << std::endl; return false; } 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; return false; } info.mode = variables["mode"].as(); 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; return false; } if (!variables.count("input-file")) { std::cout << "\nERROR: missing BSA archive\n\n" << desc << std::endl; return false; } 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 = std::filesystem::current_path(); if (info.mode == "extract") { if (inputFiles.size() < 2) { std::cout << "\nERROR: file to extract unspecified\n\n" << desc << std::endl; return false; } 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 (inputFiles.empty()) { std::cout << "\nERROR: file to add unspecified\n\n" << desc << std::endl; return false; } 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 (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; return true; } template int list(std::unique_ptr& bsa, Arguments& info) { // List all files const auto& files = bsa->getList(); for (const auto& file : files) { if (info.longformat) { // Long format std::ios::fmtflags f(std::cout.flags()); std::cout << std::setw(50) << std::left << file.name(); std::cout << std::setw(8) << std::left << std::dec << file.fileSize; std::cout << "@ 0x" << std::hex << file.offset << std::endl; std::cout.flags(f); } else std::cout << file.name() << std::endl; } return 0; } template int extract(std::unique_ptr& bsa, Arguments& info) { auto archivePath = info.extractfile.u8string(); Misc::StringUtils::replaceAll(archivePath, u8"/", u8"\\"); auto extractPath = info.extractfile.u8string(); Misc::StringUtils::replaceAll(extractPath, u8"\\", u8"/"); Files::IStreamPtr stream; // Get a stream for the file to extract for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it) { 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) std::filesystem::path relPath(extractPath); std::filesystem::path target; if (info.fullpath) target = info.outdir / relPath; else target = info.outdir / relPath.filename(); // Create the directory hierarchy std::filesystem::create_directories(target.parent_path()); std::filesystem::file_status s = std::filesystem::status(target.parent_path()); if (!std::filesystem::is_directory(s)) { std::cout << "ERROR: " << Files::pathToUnicodeString(target.parent_path()) << " is not a directory." << std::endl; return 3; } std::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << Files::pathToUnicodeString(info.extractfile) << " to " << Files::pathToUnicodeString(target) << std::endl; out << stream->rdbuf(); out.close(); return 0; } template int extractAll(std::unique_ptr& bsa, Arguments& info) { 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) auto target = info.outdir; target /= Misc::StringUtils::stringToU8String(extractPath); // Create the directory hierarchy std::filesystem::create_directories(target.parent_path()); 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 Files::IStreamPtr data = bsa->getFile(&file); std::ofstream out(target, std::ios::binary); // Write the file to disk std::cout << "Extracting " << Files::pathToUnicodeString(target) << std::endl; out << data->rdbuf(); out.close(); } return 0; } template int add(std::unique_ptr& bsa, Arguments& info) { 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; } } openmw-openmw-0.49.0/apps/bulletobjecttool/000077500000000000000000000000001503074453300207375ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/bulletobjecttool/CMakeLists.txt000066400000000000000000000012251503074453300234770ustar00rootroot00000000000000set(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() openmw-openmw-0.49.0/apps/bulletobjecttool/main.cpp000066400000000000000000000210741503074453300223730ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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() { 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"); 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.processPaths(variables, std::filesystem::current_path()); 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); } openmw-openmw-0.49.0/apps/components_tests/000077500000000000000000000000001503074453300207725ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/CMakeLists.txt000066400000000000000000000066321503074453300235410ustar00rootroot00000000000000include_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 esm3/testcstringids.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() openmw-openmw-0.49.0/apps/components_tests/detournavigator/000077500000000000000000000000001503074453300242075ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp000066400000000000000000001041351503074453300310030ustar00rootroot00000000000000#include "settings.hpp" #include #include #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); constexpr VFS::Path::NormalizedView test("test.nif"); bulletShape->mFileName = test; 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); } } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteRecastMeshToFile = true; const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.obj")); } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_recast_mesh_with_revision) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteRecastMeshToFile = true; mSettings.mEnableRecastMeshFileNameRevision = true; const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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_TRUE(std::filesystem::exists(dir / "0.0.recastmesh.1.2.obj")); } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_recast_mesh_to_absent_file_should_not_fail_tile_generation) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteRecastMeshToFile = true; const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; mSettings.mRecastMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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); EXPECT_FALSE(std::filesystem::exists(dir)); } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteNavMeshToFile = true; const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.bin")); } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, should_write_debug_navmesh_with_revision) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteNavMeshToFile = true; mSettings.mEnableNavMeshFileNameRevision = true; const std::filesystem::path dir = TestingOpenMW::outputDirPath("DetourNavigatorAsyncNavMeshUpdaterTest"); mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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_TRUE(std::filesystem::exists(dir / "all_tiles_navmesh.1.1.bin")); } TEST_F(DetourNavigatorAsyncNavMeshUpdaterTest, writing_navmesh_to_absent_file_should_not_fail_tile_generation) { mRecastMeshManager.setWorldspace(mWorldspace, nullptr); addHeightFieldPlane(mRecastMeshManager); mSettings.mEnableWriteNavMeshToFile = true; const std::filesystem::path dir = TestingOpenMW::outputDir() / "absent"; mSettings.mNavMeshPathPrefix = Files::pathToUnicodeString(dir) + "/"; Log(Debug::Verbose) << mSettings.mRecastMeshPathPrefix; 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); EXPECT_FALSE(std::filesystem::exists(dir)); } 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); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/generate.hpp000066400000000000000000000026621503074453300265200ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/components_tests/detournavigator/gettilespositions.cpp000066400000000000000000000154711503074453300305130ustar00rootroot00000000000000#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)); } openmw-openmw-0.49.0/apps/components_tests/detournavigator/navigator.cpp000066400000000000000000001612651503074453300267200ustar00rootroot00000000000000#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; } MATCHER_P4(Vec3fEq, x, y, z, precision, "") { return std::abs(arg.x() - x) < precision && std::abs(arg.y() - y) < precision && std::abs(arg.z() - z) < precision; } 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_TRUE(result.has_value()); EXPECT_THAT(*result, Vec3fEq(70.35845947265625, 335.592041015625, -2.6667339801788330078125, 1)) << *result; const auto distance = (*result - mStart).length(); EXPECT_NEAR(distance, 125.80865478515625, 1) << 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)); } openmw-openmw-0.49.0/apps/components_tests/detournavigator/navmeshdb.cpp000066400000000000000000000166621503074453300266750ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/navmeshtilescache.cpp000066400000000000000000000421341503074453300304050ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/operators.hpp000066400000000000000000000053211503074453300267370ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/components_tests/detournavigator/recastmeshbuilder.cpp000066400000000000000000000637471503074453300304410ustar00rootroot00000000000000#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 })); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/recastmeshobject.cpp000066400000000000000000000067371503074453300302550ustar00rootroot00000000000000 #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)); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/settings.hpp000066400000000000000000000036321503074453300265640ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/components_tests/detournavigator/settingsutils.cpp000066400000000000000000000040561503074453300276410ustar00rootroot00000000000000 #include #include namespace { using namespace testing; using namespace DetourNavigator; struct DetourNavigatorGetTilePositionTest : Test { RecastSettings mSettings; DetourNavigatorGetTilePositionTest() { mSettings.mCellSize = 0.5; mSettings.mTileSize = 64; } }; TEST_F(DetourNavigatorGetTilePositionTest, for_zero_coordinates_should_return_zero_tile_position) { EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(0, 0, 0)), TilePosition(0, 0)); } TEST_F(DetourNavigatorGetTilePositionTest, tile_size_should_be_multiplied_by_cell_size) { EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 0, 0)), TilePosition(1, 0)); } TEST_F(DetourNavigatorGetTilePositionTest, tile_position_calculates_by_floor) { EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(31, 0, 0)), TilePosition(0, 0)); } TEST_F(DetourNavigatorGetTilePositionTest, tile_position_depends_on_x_and_z_coordinates) { EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(32, 64, 128)), TilePosition(1, 4)); } TEST_F(DetourNavigatorGetTilePositionTest, tile_position_works_for_negative_coordinates) { EXPECT_EQ(getTilePosition(mSettings, osg::Vec3f(-31, 0, -32)), TilePosition(-1, -1)); } struct DetourNavigatorMakeTileBoundsTest : Test { RecastSettings mSettings; DetourNavigatorMakeTileBoundsTest() { mSettings.mCellSize = 0.5; mSettings.mTileSize = 64; } }; 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) })); } 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) })); } } openmw-openmw-0.49.0/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp000066400000000000000000000622221503074453300324160ustar00rootroot00000000000000 #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); } } openmw-openmw-0.49.0/apps/components_tests/esm/000077500000000000000000000000001503074453300215565ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/esm/test_fixed_string.cpp000066400000000000000000000136071503074453300260150ustar00rootroot00000000000000#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) { constexpr std::string_view someStr = "some string"; { SCOPED_TRACE("4 bytes"); ESM::NAME empty = ESM::NAME(); EXPECT_TRUE(empty == ""); EXPECT_TRUE(empty == static_cast(0)); EXPECT_TRUE(empty != someStr); EXPECT_TRUE(empty != static_cast(42)); } { SCOPED_TRACE("32 bytes"); ESM::NAME32 empty = ESM::NAME32(); EXPECT_TRUE(empty == ""); EXPECT_TRUE(empty != someStr); } } 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)); } } openmw-openmw-0.49.0/apps/components_tests/esm/testrefid.cpp000066400000000000000000000462701503074453300242640ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/esm/variant.cpp000066400000000000000000000401341503074453300237300ustar00rootroot00000000000000#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 })); } openmw-openmw-0.49.0/apps/components_tests/esm3/000077500000000000000000000000001503074453300216415ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/esm3/readerscache.cpp000066400000000000000000000103771503074453300247660ustar00rootroot00000000000000#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); } } TEST_F(ESM3ReadersCacheWithContentFile, CachedSizeAndName) { ESM::ReadersCache readers(2); { readers.get(0)->openRaw(std::make_unique("123"), "closed0.omwaddon"); readers.get(1)->openRaw(std::make_unique("12345"), "closed1.omwaddon"); readers.get(2)->openRaw(std::make_unique("1234567"), "free.omwaddon"); } auto busy = readers.get(3); busy->openRaw(std::make_unique("123456789"), "busy.omwaddon"); EXPECT_EQ(readers.getFileSize(0), 3); EXPECT_EQ(readers.getName(0), "closed0.omwaddon"); EXPECT_EQ(readers.getFileSize(1), 5); EXPECT_EQ(readers.getName(1), "closed1.omwaddon"); EXPECT_EQ(readers.getFileSize(2), 7); EXPECT_EQ(readers.getName(2), "free.omwaddon"); EXPECT_EQ(readers.getFileSize(3), 9); EXPECT_EQ(readers.getName(3), "busy.omwaddon"); // not-yet-seen indices give zero for their size EXPECT_EQ(readers.getFileSize(4), 0); } } openmw-openmw-0.49.0/apps/components_tests/esm3/testcstringids.cpp000066400000000000000000000034141503074453300254200ustar00rootroot00000000000000#include #include #include #include namespace ESM { namespace { TEST(Esm3CStringIdTest, dialNameShouldBeNullTerminated) { std::unique_ptr stream; { auto ostream = std::make_unique(); ESMWriter writer; writer.setFormatVersion(DefaultFormatVersion); writer.save(*ostream); Dialogue record; record.blank(); record.mStringId = "topic name"; record.mId = RefId::stringRefId(record.mStringId); record.mType = Dialogue::Topic; writer.startRecord(Dialogue::sRecordId); record.save(writer); writer.endRecord(Dialogue::sRecordId); stream = std::move(ostream); } ESMReader reader; reader.open(std::move(stream), "stream"); ASSERT_TRUE(reader.hasMoreRecs()); ASSERT_EQ(reader.getRecName(), Dialogue::sRecordId); reader.getRecHeader(); while (reader.hasMoreSubs()) { reader.getSubName(); if (reader.retSubName().toInt() == SREC_NAME) { reader.getSubHeader(); auto size = reader.getSubSize(); std::string buffer(size, '1'); reader.getExact(buffer.data(), size); ASSERT_EQ(buffer[size - 1], '\0'); return; } else reader.skipHSub(); } ASSERT_FALSE(true); } } } openmw-openmw-0.49.0/apps/components_tests/esm3/testesmwriter.cpp000066400000000000000000000055011503074453300252670ustar00rootroot00000000000000#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, writeHRefIdShouldProduceCertainNumberOfBytes) { 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)); } } openmw-openmw-0.49.0/apps/components_tests/esm3/testinfoorder.cpp000066400000000000000000000011051503074453300252310ustar00rootroot00000000000000#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); } } } openmw-openmw-0.49.0/apps/components_tests/esm3/testsaveload.cpp000066400000000000000000000772121503074453300250540ustar00rootroot00000000000000#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())); } } openmw-openmw-0.49.0/apps/components_tests/esm4/000077500000000000000000000000001503074453300216425ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/esm4/includes.cpp000066400000000000000000000066161503074453300241650ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include openmw-openmw-0.49.0/apps/components_tests/esmloader/000077500000000000000000000000001503074453300227455ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/esmloader/esmdata.cpp000066400000000000000000000101171503074453300250670ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/esmloader/load.cpp000066400000000000000000000121701503074453300243710ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/esmloader/record.cpp000066400000000000000000000042661503074453300247370ustar00rootroot00000000000000#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)); } openmw-openmw-0.49.0/apps/components_tests/esmterrain/000077500000000000000000000000001503074453300231435ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/esmterrain/testgridsampling.cpp000066400000000000000000000704311503074453300272340ustar00rootroot00000000000000#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 })); } } } openmw-openmw-0.49.0/apps/components_tests/files/000077500000000000000000000000001503074453300220745ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/files/conversion_tests.cpp000066400000000000000000000017171503074453300262150ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/files/hash.cpp000066400000000000000000000052071503074453300235270ustar00rootroot00000000000000#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 = outputFilePath("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 = outputFilePath("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 } })); } openmw-openmw-0.49.0/apps/components_tests/fx/000077500000000000000000000000001503074453300214075ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/fx/lexer.cpp000066400000000000000000000141441503074453300232360ustar00rootroot00000000000000#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())); } } openmw-openmw-0.49.0/apps/components_tests/fx/technique.cpp000066400000000000000000000154311503074453300241040ustar00rootroot00000000000000#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")); } } openmw-openmw-0.49.0/apps/components_tests/lua/000077500000000000000000000000001503074453300215535ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/lua/test_async.cpp000066400000000000000000000035411503074453300244360ustar00rootroot00000000000000#include #include #include #include #include namespace { using namespace testing; struct LuaCoroutineCallbackTest : Test { void SetUp() override { mLua.protectedCall([&](LuaUtil::LuaView& view) { sol::table hiddenData(view.sol(), sol::create); hiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = LuaUtil::ScriptId{}; view.sol()["async"] = LuaUtil::getAsyncPackageInitializer( view.sol(), []() { return 0.; }, []() { return 0.; })(hiddenData); view.sol()["pass"] = [&](const sol::table& t) { mCb = LuaUtil::Callback::fromLua(t); }; }); } 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(async: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(async:callback(function() error('COROUTINE CALLBACK') end)) end)() )X"); view.sol().collect_garbage(); }); EXPECT_ERROR(mCb.call(), "COROUTINE CALLBACK"); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_configuration.cpp000066400000000000000000000312521503074453300261700ustar00rootroot00000000000000#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); } } } openmw-openmw-0.49.0/apps/components_tests/lua/test_inputactions.cpp000066400000000000000000000050241503074453300260370ustar00rootroot00000000000000#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), false }); registry.insert(a); LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description", sol::make_object(lua, false), 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()); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_l10n.cpp000066400000000000000000000202151503074453300240700ustar00rootroot00000000000000#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!"); }); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_lua.cpp000066400000000000000000000235771503074453300241150ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_scriptscontainer.cpp000066400000000000000000000562351503074453300267230ustar00rootroot00000000000000#include #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"); constexpr VFS::Path::NormalizedView unloadPath("unload.lua"); VFSTestFile unloadScript(R"X( x = 0 y = 0 z = 0 return { engineHandlers = { onSave = function(state) print('saving', x, y, z) return {x = x, y = y} end, onLoad = function(state) x, y = state.x, state.y print('loaded', x, y, z) end }, eventHandlers = { Set = function(eventData) x, y, z = eventData.x, eventData.y, eventData.z end } } )X"); constexpr VFS::Path::NormalizedView customDataPath("customdata.lua"); VFSTestFile customDataScript(R"X( data = nil return { engineHandlers = { onSave = function() return data end, onLoad = function(state) data = state end, onInit = function(state) data = state end }, eventHandlers = { WakeUp = function() 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 }, { unloadPath, &unloadScript }, { customDataPath, &customDataScript }, }); 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 CUSTOM: unload.lua CUSTOM: customdata.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; } TEST_F(LuaScriptsContainerTest, Unload) { LuaUtil::ScriptTracker tracker; LuaUtil::ScriptsContainer scripts1(&mLua, "Test", &tracker, false); EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId(unloadPath))); EXPECT_EQ(tracker.size(), 1); mLua.protectedCall([&](LuaUtil::LuaView& lua) { scripts1.receiveEvent("Set", LuaUtil::serialize(lua.sol().create_table_with("x", 3, "y", 2, "z", 1))); testing::internal::CaptureStdout(); for (int i = 0; i < 600; ++i) tracker.unloadInactiveScripts(lua); EXPECT_EQ(tracker.size(), 0); scripts1.receiveEvent("Set", LuaUtil::serialize(lua.sol().create_table_with("x", 10, "y", 20, "z", 30))); EXPECT_EQ(internal::GetCapturedStdout(), "Test[unload.lua]:\tsaving\t3\t2\t1\n" "Test[unload.lua]:\tloaded\t3\t2\t0\n"); }); EXPECT_EQ(tracker.size(), 1); ESM::LuaScripts data; scripts1.save(data); EXPECT_EQ(tracker.size(), 1); mLua.protectedCall([&](LuaUtil::LuaView& lua) { for (int i = 0; i < 600; ++i) tracker.unloadInactiveScripts(lua); }); EXPECT_EQ(tracker.size(), 0); scripts1.load(data); EXPECT_EQ(tracker.size(), 0); } TEST_F(LuaScriptsContainerTest, LoadOrderChange) { LuaUtil::ScriptTracker tracker; LuaUtil::ScriptsContainer scripts1(&mLua, "Test", &tracker, false); LuaUtil::BasicSerializer serializer1; LuaUtil::BasicSerializer serializer2([](int contentFileIndex) -> int { if (contentFileIndex == 12) return 34; else if (contentFileIndex == 37) return 12; return contentFileIndex; }); scripts1.setSerializer(&serializer1); scripts1.setSavedDataDeserializer(&serializer2); mLua.protectedCall([&](LuaUtil::LuaView& lua) { sol::object id1 = sol::make_object_userdata(lua.sol(), ESM::RefNum{ 42, 12 }); sol::object id2 = sol::make_object_userdata(lua.sol(), ESM::RefNum{ 13, 37 }); sol::table table = lua.newTable(); table[id1] = id2; LuaUtil::BinaryData serialized = LuaUtil::serialize(table, &serializer1); EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId(customDataPath), serialized)); EXPECT_EQ(tracker.size(), 1); for (int i = 0; i < 600; ++i) tracker.unloadInactiveScripts(lua); EXPECT_EQ(tracker.size(), 0); scripts1.receiveEvent("WakeUp", {}); EXPECT_EQ(tracker.size(), 1); }); ESM::LuaScripts data1; ESM::LuaScripts data2; scripts1.save(data1); scripts1.load(data1); scripts1.save(data2); EXPECT_NE(data1.mScripts[0].mData, data2.mScripts[0].mData); mLua.protectedCall([&](LuaUtil::LuaView& lua) { sol::object deserialized = LuaUtil::deserialize(lua.sol(), data2.mScripts[0].mData, &serializer1); EXPECT_TRUE(deserialized.is()); sol::table table = deserialized; for (const auto& [key, value] : table) { EXPECT_TRUE(key.is()); EXPECT_TRUE(value.is()); EXPECT_EQ(key.as(), (ESM::RefNum{ 42, 34 })); EXPECT_EQ(value.as(), (ESM::RefNum{ 13, 12 })); return; } EXPECT_FALSE(true); }); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_serialization.cpp000066400000000000000000000250451503074453300262010ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_storage.cpp000066400000000000000000000122171503074453300247650ustar00rootroot00000000000000#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")); }); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_ui_content.cpp000066400000000000000000000111671503074453300254730ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_utilpackage.cpp000066400000000000000000000313321503074453300256110ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/components_tests/lua/test_yaml.cpp000066400000000000000000000316071503074453300242670ustar00rootroot00000000000000#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")); } } openmw-openmw-0.49.0/apps/components_tests/main.cpp000066400000000000000000000017671503074453300224350ustar00rootroot00000000000000#include #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); const int result = RUN_ALL_TESTS(); if (result == 0) std::filesystem::remove_all(TestingOpenMW::outputDir()); return result; } openmw-openmw-0.49.0/apps/components_tests/misc/000077500000000000000000000000001503074453300217255ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/misc/compression.cpp000066400000000000000000000015461503074453300250000ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/misc/progressreporter.cpp000066400000000000000000000020461503074453300260620ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/misc/test_endianness.cpp000066400000000000000000000063001503074453300256160ustar00rootroot00000000000000#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))); } openmw-openmw-0.49.0/apps/components_tests/misc/test_resourcehelpers.cpp000066400000000000000000000045161503074453300267100ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/components_tests/misc/test_stringops.cpp000066400000000000000000000175631503074453300255340ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/misc/testmathutil.cpp000066400000000000000000000163551503074453300251720ustar00rootroot00000000000000#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::PIf), }, { osg::Quat(0, 1, 0, 0), osg::Vec3f(0, 0, 0), }, { osg::Quat(0, 0, 1, 0), osg::Vec3f(0, 0, osg::PIf), }, { 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::PIf, 0, 0), }, { osg::Quat(0, 1, 0, 0), osg::Vec3f(osg::PIf, 0, osg::PIf), }, { osg::Quat(0, 0, 1, 0), osg::Vec3f(0, 0, osg::PIf), }, { 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)); } } openmw-openmw-0.49.0/apps/components_tests/nif/000077500000000000000000000000001503074453300215465ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/nif/node.hpp000066400000000000000000000032531503074453300232070ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/components_tests/nifloader/000077500000000000000000000000001503074453300227355ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/nifloader/testbulletnifloader.cpp000066400000000000000000001572441503074453300275310ustar00rootroot00000000000000#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; constexpr VFS::Path::NormalizedView testNif("test.nif"); constexpr VFS::Path::NormalizedView xtestNif("xtest.nif"); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(xtestNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(xtestNif); 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(xtestNif); 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(xtestNif); 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(xtestNif); 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(testNif); 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(testNif); 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(xtestNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(testNif); 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(xtestNif); 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(xtestNif); 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(xtestNif); 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(testNif); 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); } } openmw-openmw-0.49.0/apps/components_tests/nifosg/000077500000000000000000000000001503074453300222575ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/nifosg/testnifloader.cpp000066400000000000000000000210441503074453300256270ustar00rootroot00000000000000#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; constexpr VFS::Path::NormalizedView testNif("test.nif"); 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(testNif); 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 Function LEQUAL } 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(testNif); 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(testNif); 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)); } openmw-openmw-0.49.0/apps/components_tests/resource/000077500000000000000000000000001503074453300226215ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/resource/testobjectcache.cpp000066400000000000000000000360341503074453300264650ustar00rootroot00000000000000#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)); } } } openmw-openmw-0.49.0/apps/components_tests/sceneutil/000077500000000000000000000000001503074453300227655ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/sceneutil/osgacontroller.cpp000066400000000000000000000140001503074453300265210ustar00rootroot00000000000000#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 } } openmw-openmw-0.49.0/apps/components_tests/serialization/000077500000000000000000000000001503074453300236475ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/serialization/binaryreader.cpp000066400000000000000000000067631503074453300270360ustar00rootroot00000000000000#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()); } } openmw-openmw-0.49.0/apps/components_tests/serialization/binarywriter.cpp000066400000000000000000000043271503074453300271020ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/serialization/format.hpp000066400000000000000000000044011503074453300256470ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/components_tests/serialization/integration.cpp000066400000000000000000000042241503074453300267000ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/serialization/sizeaccumulator.cpp000066400000000000000000000023251503074453300275670ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/settings/000077500000000000000000000000001503074453300226325ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/settings/parser.cpp000066400000000000000000000263131503074453300246370ustar00rootroot00000000000000#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" } })); }); } } openmw-openmw-0.49.0/apps/components_tests/settings/shadermanager.cpp000066400000000000000000000041531503074453300261420ustar00rootroot00000000000000#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()); }); } } openmw-openmw-0.49.0/apps/components_tests/settings/testvalues.cpp000066400000000000000000000126541503074453300255450ustar00rootroot00000000000000#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()); } } } openmw-openmw-0.49.0/apps/components_tests/shader/000077500000000000000000000000001503074453300222405ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/shader/parsedefines.cpp000066400000000000000000000146021503074453300254170ustar00rootroot00000000000000#include #include #include namespace { using namespace testing; using namespace Shader; using DefineMap = ShaderManager::DefineMap; struct ShaderParseDefinesTest : Test { std::string mSource; const std::string mName = "shader"; DefineMap mDefines; DefineMap mGlobalDefines; }; TEST_F(ShaderParseDefinesTest, empty_should_succeed) { ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, ""); } TEST_F(ShaderParseDefinesTest, should_fail_for_absent_define) { mSource = "@foo\n"; ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "@foo\n"); } TEST_F(ShaderParseDefinesTest, should_replace_by_existing_define) { mDefines["foo"] = "42"; mSource = "@foo\n"; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42\n"); } TEST_F(ShaderParseDefinesTest, should_replace_by_existing_global_define) { mGlobalDefines["foo"] = "42"; mSource = "@foo\n"; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42\n"); } TEST_F(ShaderParseDefinesTest, should_prefer_define_over_global_define) { mDefines["foo"] = "13"; mGlobalDefines["foo"] = "42"; mSource = "@foo\n"; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "13\n"); } namespace SupportedTerminals { struct ShaderParseDefinesTest : ::ShaderParseDefinesTest, WithParamInterface { }; TEST_P(ShaderParseDefinesTest, support_defines_terminated_by) { mDefines["foo"] = "13"; mSource = "@foo" + std::string(1, GetParam()); ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "13" + std::string(1, GetParam())); } INSTANTIATE_TEST_SUITE_P( SupportedTerminals, ShaderParseDefinesTest, Values(' ', '\n', '\r', '(', ')', '[', ']', '.', ';', ',')); } TEST_F(ShaderParseDefinesTest, should_not_support_define_ending_with_source) { mDefines["foo"] = "42"; mSource = "@foo"; ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "@foo"); } TEST_F(ShaderParseDefinesTest, should_replace_all_matched_values) { mDefines["foo"] = "42"; mSource = "@foo @foo "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42 42 "); } TEST_F(ShaderParseDefinesTest, should_support_define_with_empty_name) { mDefines[""] = "42"; mSource = "@ "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42 "); } TEST_F(ShaderParseDefinesTest, should_replace_all_found_defines) { mDefines["foo"] = "42"; mDefines["bar"] = "13"; mDefines["baz"] = "55"; mSource = "@foo @bar "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42 13 "); } TEST_F(ShaderParseDefinesTest, should_fail_on_foreach_without_endforeach) { mSource = "@foreach "; ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach "); } TEST_F(ShaderParseDefinesTest, should_fail_on_endforeach_without_foreach) { mSource = "@endforeach "; ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$endforeach "); } TEST_F(ShaderParseDefinesTest, should_replace_at_sign_by_dollar_for_foreach_endforeach) { mSource = "@foreach @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach $endforeach "); } TEST_F(ShaderParseDefinesTest, should_succeed_on_unmatched_nested_foreach) { mSource = "@foreach @foreach @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach $foreach $endforeach "); } TEST_F(ShaderParseDefinesTest, should_fail_on_unmatched_nested_endforeach) { mSource = "@foreach @endforeach @endforeach "; ASSERT_FALSE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach $endforeach $endforeach "); } TEST_F(ShaderParseDefinesTest, should_support_nested_foreach) { mSource = "@foreach @foreach @endforeach @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach $foreach $endforeach $endforeach "); } TEST_F(ShaderParseDefinesTest, should_support_foreach_variable) { mSource = "@foreach foo @foo @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach foo $foo $endforeach "); } TEST_F(ShaderParseDefinesTest, should_not_replace_foreach_variable_by_define) { mDefines["foo"] = "42"; mSource = "@foreach foo @foo @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach foo $foo $endforeach "); } TEST_F(ShaderParseDefinesTest, should_support_nested_foreach_with_variable) { mSource = "@foreach foo @foo @foreach bar @bar @endforeach @endforeach "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "$foreach foo $foo $foreach bar $bar $endforeach $endforeach "); } TEST_F(ShaderParseDefinesTest, should_not_support_single_line_comments_for_defines) { mDefines["foo"] = "42"; mSource = "@foo // @foo\n"; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "42 // 42\n"); } TEST_F(ShaderParseDefinesTest, should_not_support_multiline_comments_for_defines) { mDefines["foo"] = "42"; mSource = "/* @foo */ @foo "; ASSERT_TRUE(parseDefines(mSource, mDefines, mGlobalDefines, mName)); EXPECT_EQ(mSource, "/* 42 */ 42 "); } } openmw-openmw-0.49.0/apps/components_tests/shader/parsefors.cpp000066400000000000000000000060211503074453300247470ustar00rootroot00000000000000#include #include #include #include namespace { using namespace testing; using namespace Shader; using DefineMap = ShaderManager::DefineMap; struct ShaderParseForsTest : Test { std::string mSource; 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)); EXPECT_EQ(mSource, ""); } TEST_F(ShaderParseForsTest, should_fail_for_single_escape_symbol) { mSource = "$"; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$"); } TEST_F(ShaderParseForsTest, should_fail_on_first_found_escaped_not_foreach) { mSource = "$foo "; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$foo "); } TEST_F(ShaderParseForsTest, should_fail_on_absent_foreach_variable) { mSource = "$foreach "; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$foreach "); } TEST_F(ShaderParseForsTest, should_fail_on_unmatched_after_variable) { mSource = "$foreach foo "; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$foreach foo "); } TEST_F(ShaderParseForsTest, should_fail_on_absent_newline_after_foreach_list) { mSource = "$foreach foo 1,2,3 "; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$foreach foo 1,2,3 "); } TEST_F(ShaderParseForsTest, should_fail_on_absent_endforeach_after_newline) { mSource = "$foreach foo 1,2,3\n"; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "$foreach foo 1,2,3\n"); } TEST_F(ShaderParseForsTest, should_replace_complete_foreach_by_line_number) { mSource = "$foreach foo 1,2,3\n$endforeach"; ASSERT_TRUE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "\n#line 3"); } TEST_F(ShaderParseForsTest, should_replace_loop_variable) { mSource = "$foreach foo 1,2,3\n$foo\n$endforeach"; ASSERT_TRUE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "1\n2\n3\n\n#line 4"); } TEST_F(ShaderParseForsTest, should_count_line_number_from_existing) { mSource = "$foreach foo 1,2,3\n#line 10\n$foo\n$endforeach"; ASSERT_TRUE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "#line 10\n1\n#line 10\n2\n#line 10\n3\n\n#line 12"); } TEST_F(ShaderParseForsTest, should_not_support_nested_loops) { mSource = "$foreach foo 1,2\n$foo\n$foreach bar 1,2\n$bar\n$endforeach\n$endforeach"; ASSERT_FALSE(parseFors(mSource, mName)); EXPECT_EQ(mSource, "1\n1\n2\n$foreach bar 1,2\n1\n\n#line 6\n2\n2\n$foreach bar 1,2\n2\n\n#line 6\n\n#line 7"); } } openmw-openmw-0.49.0/apps/components_tests/shader/parselinks.cpp000066400000000000000000000050221503074453300251160ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/components_tests/shader/shadermanager.cpp000066400000000000000000000202531503074453300255470ustar00rootroot00000000000000#include #include #include #include #include namespace { using namespace testing; using namespace Shader; struct ShaderManagerTest : Test { ShaderManager mManager; ShaderManager::DefineMap mDefines; ShaderManagerTest() { mManager.setShaderPath(TestingOpenMW::outputDir()); } 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)); }); } } openmw-openmw-0.49.0/apps/components_tests/sqlite3/000077500000000000000000000000001503074453300223565ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/sqlite3/db.cpp000066400000000000000000000005051503074453300234470ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/components_tests/sqlite3/request.cpp000066400000000000000000000226111503074453300245540ustar00rootroot00000000000000#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))); } } openmw-openmw-0.49.0/apps/components_tests/sqlite3/statement.cpp000066400000000000000000000013041503074453300250640ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/components_tests/sqlite3/transaction.cpp000066400000000000000000000033321503074453300254100ustar00rootroot00000000000000#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))); } } openmw-openmw-0.49.0/apps/components_tests/toutf8/000077500000000000000000000000001503074453300222235ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/toutf8/data/000077500000000000000000000000001503074453300231345ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/toutf8/data/french-utf8.txt000066400000000000000000000001531503074453300260250ustar00rootroot00000000000000Vous lui donnez le gâteau sans protester avant d’aller chercher tous vos amis et de revenir vous venger.openmw-openmw-0.49.0/apps/components_tests/toutf8/data/french-win1252.txt000066400000000000000000000001501503074453300262430ustar00rootroot00000000000000Vous lui donnez le gteau sans protester avant daller chercher tous vos amis et de revenir vous venger.openmw-openmw-0.49.0/apps/components_tests/toutf8/data/russian-utf8.txt000066400000000000000000000003311503074453300262420ustar00rootroot00000000000000Без вопросов отдаете ему рулет, зная, что позже вы сможете привести с собой своих друзей и тогда он получит по заслугам?openmw-openmw-0.49.0/apps/components_tests/toutf8/data/russian-win1251.txt000066400000000000000000000001701503074453300264630ustar00rootroot00000000000000 , , ?openmw-openmw-0.49.0/apps/components_tests/toutf8/toutf8.cpp000066400000000000000000000136171503074453300241700ustar00rootroot00000000000000#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()); } } openmw-openmw-0.49.0/apps/components_tests/vfs/000077500000000000000000000000001503074453300215705ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/components_tests/vfs/testpathutil.cpp000066400000000000000000000154641503074453300250400ustar00rootroot00000000000000#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"); } } } openmw-openmw-0.49.0/apps/doc.hpp000066400000000000000000000001151503074453300166360ustar00rootroot00000000000000// Note: This is not a regular source file. /// \defgroup apps Applications openmw-openmw-0.49.0/apps/esmtool/000077500000000000000000000000001503074453300170455ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/esmtool/.gitignore000066400000000000000000000000531503074453300210330ustar00rootroot00000000000000*.esp *.esm *.ess *_test esmtool *_raw.txt openmw-openmw-0.49.0/apps/esmtool/CMakeLists.txt000066400000000000000000000012531503074453300216060ustar00rootroot00000000000000set(ESMTOOL 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} ) target_link_libraries(esmtool Boost::program_options components ) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(esmtool PRIVATE --coverage) target_link_libraries(esmtool gcov) endif() if (WIN32) install(TARGETS esmtool RUNTIME DESTINATION ".") endif() if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(esmtool PRIVATE ) endif() openmw-openmw-0.49.0/apps/esmtool/arguments.hpp000066400000000000000000000011161503074453300215620ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/esmtool/esmtool.cpp000066400000000000000000000521331503074453300212370ustar00rootroot00000000000000#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" namespace { using namespace EsmTool; constexpr unsigned majorVersion = 1; constexpr unsigned minorVersion = 3; // Create a local alias for brevity namespace bpo = boost::program_options; 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. 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(); bpo::store(valid_opts, variables); } catch (std::exception& e) { std::cout << "ERROR parsing arguments: " << e.what() << std::endl; return false; } 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("type") > 0) info.types = variables["type"].as>(); 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; } 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; return true; } void loadCell(const Arguments& info, ESM::Cell& cell, ESM::ESMReader& esm, ESMData* data); int load(const Arguments& info, ESMData* data); int clone(const Arguments& info); int comp(const Arguments& info); } int main(int argc, char** argv) { try { Arguments info; if (!parseOptions(argc, argv, info)) return 1; if (info.mode == "dump") return load(info, nullptr); else if (info.mode == "clone") return clone(info); else if (info.mode == "comp") return comp(info); else { std::cout << "Invalid or no mode specified, dying horribly. Have a nice day." << std::endl; return 1; } } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; return 1; } return 0; } namespace { 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"); // 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); // Loop through all the references ESM::CellRef ref; if (!quiet) std::cout << " References:\n"; bool deleted = false; ESM::MovedCellRef movedCellRef; bool moved = false; while (cell.getNextRef(esm, ref, deleted, movedCellRef, moved)) { 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 printRawTes3(const std::filesystem::path& path) { std::cout << "TES3 RAW file listing: " << Files::pathToUnicodeString(path) << '\n'; ESM::ESMReader esm; esm.openRaw(path); while (esm.hasMoreRecs()) { 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 loadTes3(const Arguments& info, std::unique_ptr&& stream, ESMData* data) { std::cout << "Loading TES3 file: " << info.filename << '\n'; ESM::ESMReader esm; ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(info.encoding)); esm.setEncoder(&encoder); std::unordered_set skipped; try { bool quiet = (info.quiet_given || info.mode == "clone"); bool loadCells = (info.loadcells_given || info.mode == "clone"); bool save = (info.mode == "clone"); esm.open(std::move(stream), info.filename); if (data != nullptr) data->mHeader = esm.getHeader(); if (!quiet) { 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); auto record = EsmTool::RecordBase::create(n); if (record == nullptr) { 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; } record->setFlags(static_cast(flags)); record->setPrintPlain(info.plain_given); record->load(esm); // 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; if (!info.name.empty() && !Misc::StringUtils::ciEqual(info.name, record->getId())) interested = false; if (!quiet && interested) { std::cout << "\nRecord: " << n.toStringView() << " " << record->getId() << "\n" << "Record flags: " << recordFlags(record->getFlags()) << '\n'; record->print(); } if (record->getType().toInt() == ESM::REC_CELL && loadCells && interested) { loadCell(info, record->cast()->get(), esm, data); } 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; } return 0; } int load(const Arguments& info, ESMData* data) { if (info.mRawFormat.has_value() && info.mode == "dump") { switch (*info.mRawFormat) { 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; } return 0; } 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; } const ESM::Format format = ESM::readFormat(*stream); stream->seekg(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)); } std::cout << "Unsupported ESM format: " << ESM::NAME(format).toStringView() << '\n'; return -1; } int clone(const Arguments& info) { if (info.outname.empty()) { std::cout << "You need to specify an output name" << std::endl; return 1; } ESMData data; if (load(info, &data) != 0) { std::cout << "Failed to load, aborting." << std::endl; return 1; } size_t recordCount = data.mRecords.size(); int digitCount = 1; // For a nicer output if (recordCount > 0) digitCount = (int)std::log10(recordCount) + 1; std::cout << "Loaded " << recordCount << " records:\n\n"; 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'; } if (i % 3 != 0) std::cout << '\n'; std::cout << "\nSaving records to: " << Files::pathToUnicodeString(info.outname) << "...\n"; 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, std::fstream::out | std::fstream::binary); esm.save(save); int saved = 0; for (auto& record : data.mRecords) { if (i <= 0) break; const ESM::NAME typeName = record->getType(); esm.startRecord(typeName, record->getFlags()); record->save(esm); if (typeName.toInt() == ESM::REC_CELL) { 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); saved++; int perc = recordCount == 0 ? 100 : (int)((saved / (float)recordCount) * 100); if (perc % 10 == 0) { std::cerr << "\r" << perc << "%"; } } std::cout << "\rDone!" << std::endl; esm.close(); save.close(); return 0; } int comp(const Arguments& info) { if (info.filename.empty() || info.outname.empty()) { std::cout << "You need to specify two input files" << std::endl; return 1; } Arguments fileOne; Arguments fileTwo; fileOne.mode = "clone"; fileTwo.mode = "clone"; fileOne.encoding = info.encoding; fileTwo.encoding = info.encoding; 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; } ESMData dataTwo; if (load(fileTwo, &dataTwo) != 0) { std::cout << "Failed to load " << Files::pathToUnicodeString(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; } return 0; } } openmw-openmw-0.49.0/apps/esmtool/labels.cpp000066400000000000000000000700521503074453300210170ustar00rootroot00000000000000#include "labels.hpp" #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 constexpr std::string_view bodyPartLabels[] = { "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 Shoulder", "Left Shoulder", "Weapon", "Tail", }; return bodyPartLabels[idx]; } else return "Invalid"; } std::string_view meshPartLabel(int idx) { if (idx >= 0 && idx <= ESM::BodyPart::MP_Tail) { static constexpr std::string_view meshPartLabels[] = { "Head", "Hair", "Neck", "Chest", "Groin", "Hand", "Wrist", "Forearm", "Upperarm", "Foot", "Ankle", "Knee", "Upper Leg", "Clavicle", "Tail", }; return meshPartLabels[idx]; } else return "Invalid"; } std::string_view meshTypeLabel(int idx) { if (idx >= 0 && idx <= ESM::BodyPart::MT_Armor) { static constexpr std::string_view meshTypeLabels[] = { "Skin", "Clothing", "Armor", }; return meshTypeLabels[idx]; } else return "Invalid"; } std::string_view clothingTypeLabel(int idx) { if (idx >= 0 && idx <= 9) { static constexpr std::string_view clothingTypeLabels[] = { "Pants", "Shoes", "Shirt", "Belt", "Robe", "Right Glove", "Left Glove", "Skirt", "Ring", "Amulet", }; return clothingTypeLabels[idx]; } else return "Invalid"; } std::string_view armorTypeLabel(int idx) { if (idx >= 0 && idx <= 10) { static constexpr std::string_view armorTypeLabels[] = { "Helmet", "Cuirass", "Left Pauldron", "Right Pauldron", "Greaves", "Boots", "Left Gauntlet", "Right Gauntlet", "Shield", "Left Bracer", "Right Bracer", }; return armorTypeLabels[idx]; } else return "Invalid"; } std::string_view dialogTypeLabel(int idx) { if (idx >= 0 && idx <= 4) { static constexpr std::string_view dialogTypeLabels[] = { "Topic", "Voice", "Greeting", "Persuasion", "Journal", }; return dialogTypeLabels[idx]; } else if (idx == -1) return "Deleted"; else return "Invalid"; } std::string_view questStatusLabel(int idx) { if (idx >= 0 && idx <= 4) { static constexpr std::string_view questStatusLabels[] = { "None", "Name", "Finished", "Restart", "Deleted", }; return questStatusLabels[idx]; } else return "Invalid"; } std::string_view creatureTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { static constexpr std::string_view creatureTypeLabels[] = { "Creature", "Daedra", "Undead", "Humanoid", }; return creatureTypeLabels[idx]; } else return "Invalid"; } std::string_view soundTypeLabel(int idx) { if (idx >= 0 && idx <= 7) { static constexpr std::string_view soundTypeLabels[] = { "Left Foot", "Right Foot", "Swim Left", "Swim Right", "Moan", "Roar", "Scream", "Land", }; return soundTypeLabels[idx]; } else return "Invalid"; } std::string_view weaponTypeLabel(int idx) { if (idx >= 0 && idx <= 13) { static constexpr std::string_view weaponTypeLabels[] = { "Short Blade One Hand", "Long Blade One Hand", "Long Blade Two Hand", "Blunt One Hand", "Blunt Two Close", "Blunt Two Wide", "Spear Two Wide", "Axe One Hand", "Axe Two Hand", "Marksman Bow", "Marksman Crossbow", "Marksman Thrown", "Arrow", "Bolt", }; return weaponTypeLabels[idx]; } else return "Invalid"; } std::string_view aiTypeLabel(ESM::AiPackageType type) { 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_view magicEffectLabel(int idx) { if (idx >= 0 && idx <= 142) { static constexpr std::string_view magicEffectLabels[] = { "Water Breathing", "Swift Swim", "Water Walking", "Shield", "Fire Shield", "Lightning Shield", "Frost Shield", "Burden", "Feather", "Jump", "Levitate", "SlowFall", "Lock", "Open", "Fire Damage", "Shock Damage", "Frost Damage", "Drain Attribute", "Drain Health", "Drain Magicka", "Drain Fatigue", "Drain Skill", "Damage Attribute", "Damage Health", "Damage Magicka", "Damage Fatigue", "Damage Skill", "Poison", "Weakness to Fire", "Weakness to Frost", "Weakness to Shock", "Weakness to Magicka", "Weakness to Common Disease", "Weakness to Blight Disease", "Weakness to Corprus Disease", "Weakness to Poison", "Weakness to Normal Weapons", "Disintegrate Weapon", "Disintegrate Armor", "Invisibility", "Chameleon", "Light", "Sanctuary", "Night Eye", "Charm", "Paralyze", "Silence", "Blind", "Sound", "Calm Humanoid", "Calm Creature", "Frenzy Humanoid", "Frenzy Creature", "Demoralize Humanoid", "Demoralize Creature", "Rally Humanoid", "Rally Creature", "Dispel", "Soultrap", "Telekinesis", "Mark", "Recall", "Divine Intervention", "Almsivi Intervention", "Detect Animal", "Detect Enchantment", "Detect Key", "Spell Absorption", "Reflect", "Cure Common Disease", "Cure Blight Disease", "Cure Corprus Disease", "Cure Poison", "Cure Paralyzation", "Restore Attribute", "Restore Health", "Restore Magicka", "Restore Fatigue", "Restore Skill", "Fortify Attribute", "Fortify Health", "Fortify Magicka", "Fortify Fatigue", "Fortify Skill", "Fortify Maximum Magicka", "Absorb Attribute", "Absorb Health", "Absorb Magicka", "Absorb Fatigue", "Absorb Skill", "Resist Fire", "Resist Frost", "Resist Shock", "Resist Magicka", "Resist Common Disease", "Resist Blight Disease", "Resist Corprus Disease", "Resist Poison", "Resist Normal Weapons", "Resist Paralysis", "Remove Curse", "Turn Undead", "Summon Scamp", "Summon Clannfear", "Summon Daedroth", "Summon Dremora", "Summon Ancestral Ghost", "Summon Skeletal Minion", "Summon Bonewalker", "Summon Greater Bonewalker", "Summon Bonelord", "Summon Winged Twilight", "Summon Hunger", "Summon Golden Saint", "Summon Flame Atronach", "Summon Frost Atronach", "Summon Storm Atronach", "Fortify Attack", "Command Creature", "Command Humanoid", "Bound Dagger", "Bound Longsword", "Bound Mace", "Bound Battle Axe", "Bound Spear", "Bound Longbow", "EXTRA SPELL", "Bound Cuirass", "Bound Helm", "Bound Boots", "Bound Shield", "Bound Gloves", "Corprus", "Vampirism", "Summon Centurion Sphere", "Sun Damage", "Stunted Magicka", "Summon Fabricant", "sEffectSummonCreature01", "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", "sEffectSummonCreature05", }; return magicEffectLabels[idx]; } else return "Invalid"; } std::string_view attributeLabel(int idx) { if (idx >= 0 && idx <= 7) { static constexpr std::string_view attributeLabels[] = { "Strength", "Intelligence", "Willpower", "Agility", "Speed", "Endurance", "Personality", "Luck", }; return attributeLabels[idx]; } else return "Invalid"; } std::string_view spellTypeLabel(int idx) { if (idx >= 0 && idx <= 5) { static constexpr std::string_view spellTypeLabels[] = { "Spells", "Abilities", "Blight Disease", "Disease", "Curse", "Powers", }; return spellTypeLabels[idx]; } else return "Invalid"; } std::string_view specializationLabel(int idx) { if (idx >= 0 && idx <= 2) { static constexpr std::string_view specializationLabels[] = { "Combat", "Magic", "Stealth", }; return specializationLabels[idx]; } else return "Invalid"; } std::string_view skillLabel(int idx) { if (idx >= 0 && idx <= 26) { static constexpr std::string_view skillLabels[] = { "Block", "Armorer", "Medium Armor", "Heavy Armor", "Blunt Weapon", "Long Blade", "Axe", "Spear", "Athletics", "Enchant", "Destruction", "Alteration", "Illusion", "Conjuration", "Mysticism", "Restoration", "Alchemy", "Unarmored", "Security", "Sneak", "Acrobatics", "Light Armor", "Short Blade", "Marksman", "Mercantile", "Speechcraft", "Hand-to-hand", }; return skillLabels[idx]; } else return "Invalid"; } std::string_view apparatusTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { static constexpr std::string_view apparatusTypeLabels[] = { "Mortar", "Alembic", "Calcinator", "Retort", }; return apparatusTypeLabels[idx]; } else return "Invalid"; } std::string_view rangeTypeLabel(int idx) { if (idx >= 0 && idx <= 2) { static constexpr std::string_view rangeTypeLabels[] = { "Self", "Touch", "Target", }; return rangeTypeLabels[idx]; } else return "Invalid"; } std::string_view schoolLabel(int idx) { if (idx >= 0 && idx <= 5) { static constexpr std::string_view schoolLabels[] = { "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration", }; return schoolLabels[idx]; } else return "Invalid"; } std::string_view enchantTypeLabel(int idx) { if (idx >= 0 && idx <= 3) { static constexpr std::string_view enchantTypeLabels[] = { "Cast Once", "Cast When Strikes", "Cast When Used", "Constant Effect", }; return enchantTypeLabels[idx]; } else return "Invalid"; } std::string_view ruleFunction(int idx) { if (idx >= ESM::DialogueCondition::Function_FacReactionLowest && idx <= ESM::DialogueCondition::Function_PcWerewolfKills) { static constexpr std::string_view ruleFunctions[] = { "Lowest Faction Reaction", "Highest Faction Reaction", "Rank Requirement", "NPC Reputation", "Health Percent", "Player Reputation", "NPC Level", "Player Health Percent", "Player Magicka", "Player Fatigue", "Player Attribute Strength", "Player Skill Block", "Player Skill Armorer", "Player Skill Medium Armor", "Player Skill Heavy Armor", "Player Skill Blunt Weapon", "Player Skill Long Blade", "Player Skill Axe", "Player Skill Spear", "Player Skill Athletics", "Player Skill Enchant", "Player Skill Destruction", "Player Skill Alteration", "Player Skill Illusion", "Player Skill Conjuration", "Player Skill Mysticism", "Player SKill Restoration", "Player Skill Alchemy", "Player Skill Unarmored", "Player Skill Security", "Player Skill Sneak", "Player Skill Acrobatics", "Player Skill Light Armor", "Player Skill Short Blade", "Player Skill Marksman", "Player Skill Mercantile", "Player Skill Speechcraft", "Player Skill Hand to Hand", "Player Gender", "Player Expelled from Faction", "Player Diseased (Common)", "Player Diseased (Blight)", "Player Clothing Modifier", "Player Crime Level", "Player Same Sex", "Player Same Race", "Player Same Faction", "Faction Rank Difference", "Player Detected", "Alarmed", "Choice Selected", "Player Attribute Intelligence", "Player Attribute Willpower", "Player Attribute Agility", "Player Attribute Speed", "Player Attribute Endurance", "Player Attribute Personality", "Player Attribute Luck", "Player Diseased (Corprus)", "Weather", "Player is a Vampire", "Player Level", "Attacked", "NPC Talked to Player", "Player Health", "Creature Target", "Friend Hit", "Fight", "Hello", "Alarm", "Flee", "Should Attack", "Werewolf", "Werewolf Kills", }; return ruleFunctions[idx]; } else return "Invalid"; } // The "unused flag bits" should probably be defined alongside the // defined bits in the ESM component. The names of the flag bits are // very inconsistent. 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; // 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%02X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } std::string landFlags(std::uint32_t flags) { std::string properties; 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; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } std::string creatureListFlags(int flags) { std::string properties; if (flags == 0) properties += "[None] "; if (flags & ESM::CreatureLevList::AllLevels) properties += "AllLevels "; int unused = (0xFFFFFFFF ^ ESM::CreatureLevList::AllLevels); if (flags & unused) properties += "Invalid "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; // 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 "; properties += Misc::StringUtils::format("(0x%02X)", flags); return properties; } std::string raceFlags(int flags) { std::string properties; 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } 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 "; properties += Misc::StringUtils::format("(0x%08X)", flags); return properties; } std::string weaponFlags(int flags) { std::string properties; 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 "; 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; } openmw-openmw-0.49.0/apps/esmtool/labels.hpp000066400000000000000000000050511503074453300210210ustar00rootroot00000000000000#ifndef OPENMW_ESMTOOL_LABELS_H #define OPENMW_ESMTOOL_LABELS_H #include #include #include #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_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_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 // GMSTs before dumping any of the records. // If the data format supported ordered lists of GMSTs (post 1.0), the // lists could define the valid values, their localization strings, // 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_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. std::string bodyPartFlags(int flags); std::string cellFlags(int flags); std::string containerFlags(int flags); std::string creatureFlags(int flags); std::string enchantmentFlags(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 #endif openmw-openmw-0.49.0/apps/esmtool/record.cpp000066400000000000000000001654041503074453300210410ustar00rootroot00000000000000#include "record.hpp" #include "labels.hpp" #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: " << 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; } std::string ruleString(const ESM::DialogueCondition& ss) { std::string_view type_str = "INVALID"; std::string_view func_str; 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; } 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::ostringstream stream; std::visit([&](auto value) { stream << value; }, 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::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++; } } void printTransport(const std::vector& transport) { 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; } } } namespace EsmTool { void CellState::load(ESM::ESMReader& reader, bool& deleted) { mCellState.mId = reader.getCellId(); mCellState.load(reader); if (mCellState.mHasFogOfWar) mFogState.load(reader); deleted = false; reader.skipRecord(); } std::unique_ptr RecordBase::create(const ESM::NAME type) { 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; } 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 << " 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 << " Flags: " << potionFlags(mData.mData.mFlags) << 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) { 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; } 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 << " 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; } 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; } 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; } 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 auto& power : mData.mPowers.mList) std::cout << " Power: " << power << std::endl; std::cout << " Deleted: " << mIsDeleted << std::endl; } 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; 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; } 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 << " 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() { 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; } 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 << " 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() { 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; } 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; } template <> void Record::print() { 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; } 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++) { // 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; } 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)) { 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; } 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() { 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; } 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(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() { 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; } 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) { 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; } 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 << " 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) { 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++; } i = 0; for (const ESM::Pathgrid::Edge& edge : mData.mEdges) { 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 << " 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; 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; 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; 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; for (const auto& 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::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 << " 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; 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) { std::cout << " Script:" << std::endl; std::cout << "START--------------------------------------" << std::endl; std::cout << mData.mScriptText << std::endl; std::cout << "END----------------------------------------" << std::endl; } else { std::cout << " Script: [skipped]" << std::endl; } std::cout << " Deleted: " << mIsDeleted << std::endl; } template <> void Record::print() { 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; } 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 <> 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; } } } template <> std::string Record::getId() const { return std::string(); // No ID for Cell record } 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 openmw-openmw-0.49.0/apps/esmtool/record.hpp000066400000000000000000000114261503074453300210400ustar00rootroot00000000000000#ifndef OPENMW_ESMTOOL_RECORD_H #define OPENMW_ESMTOOL_RECORD_H #include #include #include #include #include namespace ESM { class ESMReader; class ESMWriter; } namespace EsmTool { template class Record; class RecordBase { protected: std::string mId; uint32_t mFlags; ESM::NAME mType; bool mPrintPlain; public: RecordBase() : mFlags(0) , mPrintPlain(false) { } virtual ~RecordBase() = default; virtual std::string getId() const = 0; uint32_t getFlags() const { return mFlags; } void setFlags(uint32_t flags) { mFlags = flags; } ESM::NAME getType() const { return mType; } void setPrintPlain(bool plain) { mPrintPlain = plain; } virtual void load(ESM::ESMReader& esm) = 0; virtual void save(ESM::ESMWriter& esm) = 0; virtual void print() = 0; static std::unique_ptr create(ESM::NAME type); // just make it a bit shorter template 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 { T mData; bool mIsDeleted; public: Record() : mIsDeleted(false) { } std::string getId() const override { return mData.mId.toDebugString(); } T& get() { return mData; } 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 <> 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 openmw-openmw-0.49.0/apps/esmtool/tes4.cpp000066400000000000000000000552161503074453300204410ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/esmtool/tes4.hpp000066400000000000000000000003741503074453300204410ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/essimporter/000077500000000000000000000000001503074453300177375ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/essimporter/CMakeLists.txt000066400000000000000000000022411503074453300224760ustar00rootroot00000000000000set(ESSIMPORTER_FILES main.cpp importer.cpp importplayer.cpp importnpcc.cpp importcrec.cpp importcellref.cpp importinventory.cpp importklst.cpp importcntc.cpp importgame.cpp importinfo.cpp importdial.cpp importques.cpp importjour.cpp importscri.cpp importscpt.cpp importproj.cpp importsplm.cpp importercontext.cpp converter.cpp convertacdt.cpp convertnpcc.cpp convertinventory.cpp convertcrec.cpp convertcntc.cpp convertscri.cpp convertscpt.cpp convertplayer.cpp ) openmw_add_executable(openmw-essimporter ${ESSIMPORTER_FILES} ) target_link_libraries(openmw-essimporter Boost::program_options components ) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(openmw-essimporter PRIVATE --coverage) target_link_libraries(openmw-essimporter gcov) endif() if (WIN32) INSTALL(TARGETS openmw-essimporter RUNTIME DESTINATION ".") endif(WIN32) if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-essimporter PRIVATE ) endif() openmw-openmw-0.49.0/apps/essimporter/convertacdt.cpp000066400000000000000000000115601503074453300227620ustar00rootroot00000000000000#include #include #include #include #include "convertacdt.hpp" namespace ESSImport { int translateDynamicIndex(int mwIndex) { if (mwIndex == 1) return 2; else if (mwIndex == 2) return 1; return mwIndex; } void convertACDT(const ACDT& acdt, ESM::CreatureStats& cStats) { for (int i = 0; i < 3; ++i) { int writeIndex = translateDynamicIndex(i); cStats.mDynamic[writeIndex].mBase = 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) { 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) { cStats.mDead = (acsc.mFlags & Dead) != 0; } void convertNpcData(const ActorData& actorData, ESM::NpcStats& npcStats) { for (int i = 0; i < ESM::Skill::Length; ++i) { npcStats.mSkills[i].mMod = 0.f; npcStats.mSkills[i].mCurrent = actorData.mSkills[i][1]; npcStats.mSkills[i].mBase = actorData.mSkills[i][0]; } npcStats.mTimeToStartDrowning = actorData.mACDT.mBreathMeter; } void convertANIS(const ANIS& anis, ESM::AnimationState& state) { static const char* animGroups[] = { "Idle", "Idle2", "Idle3", "Idle4", "Idle5", "Idle6", "Idle7", "Idle8", "Idle9", "Idlehh", "Idle1h", "Idle2c", "Idle2w", "IdleSwim", "IdleSpell", "IdleCrossbow", "IdleSneak", "IdleStorm", "Torch", "Hit1", "Hit2", "Hit3", "Hit4", "Hit5", "SwimHit1", "SwimHit2", "SwimHit3", "Death1", "Death2", "Death3", "Death4", "Death5", "DeathKnockDown", "DeathKnockOut", "KnockDown", "KnockOut", "SwimDeath", "SwimDeath2", "SwimDeath3", "SwimDeathKnockDown", "SwimDeathKnockOut", "SwimKnockOut", "SwimKnockDown", "SwimWalkForward", "SwimWalkBack", "SwimWalkLeft", "SwimWalkRight", "SwimRunForward", "SwimRunBack", "SwimRunLeft", "SwimRunRight", "SwimTurnLeft", "SwimTurnRight", "WalkForward", "WalkBack", "WalkLeft", "WalkRight", "TurnLeft", "TurnRight", "RunForward", "RunBack", "RunLeft", "RunRight", "SneakForward", "SneakBack", "SneakLeft", "SneakRight", "Jump", "WalkForwardhh", "WalkBackhh", "WalkLefthh", "WalkRighthh", "TurnLefthh", "TurnRighthh", "RunForwardhh", "RunBackhh", "RunLefthh", "RunRighthh", "SneakForwardhh", "SneakBackhh", "SneakLefthh", "SneakRighthh", "Jumphh", "WalkForward1h", "WalkBack1h", "WalkLeft1h", "WalkRight1h", "TurnLeft1h", "TurnRight1h", "RunForward1h", "RunBack1h", "RunLeft1h", "RunRight1h", "SneakForward1h", "SneakBack1h", "SneakLeft1h", "SneakRight1h", "Jump1h", "WalkForward2c", "WalkBack2c", "WalkLeft2c", "WalkRight2c", "TurnLeft2c", "TurnRight2c", "RunForward2c", "RunBack2c", "RunLeft2c", "RunRight2c", "SneakForward2c", "SneakBack2c", "SneakLeft2c", "SneakRight2c", "Jump2c", "WalkForward2w", "WalkBack2w", "WalkLeft2w", "WalkRight2w", "TurnLeft2w", "TurnRight2w", "RunForward2w", "RunBack2w", "RunLeft2w", "RunRight2w", "SneakForward2w", "SneakBack2w", "SneakLeft2w", "SneakRight2w", "Jump2w", "SpellCast", "SpellTurnLeft", "SpellTurnRight", "Attack1", "Attack2", "Attack3", "SwimAttack1", "SwimAttack2", "SwimAttack3", "HandToHand", "Crossbow", "BowAndArrow", "ThrowWeapon", "WeaponOneHand", "WeaponTwoHand", "WeaponTwoWide", "Shield", "PickProbe", "InventoryHandToHand", "InventoryWeaponOneHand", "InventoryWeaponTwoHand", "InventoryWeaponTwoWide" }; if (anis.mGroupIndex < (sizeof(animGroups) / sizeof(*animGroups))) { std::string group(animGroups[anis.mGroupIndex]); Misc::StringUtils::lowerCaseInPlace(group); ESM::AnimationState::ScriptedAnimation scriptedAnim; scriptedAnim.mGroup = std::move(group); scriptedAnim.mTime = anis.mTime; scriptedAnim.mAbsolute = true; // Neither loop count nor queueing seems to be supported by the ess format. scriptedAnim.mLoopCount = std::numeric_limits::max(); state.mScriptedAnims.push_back(scriptedAnim); } else // TODO: Handle 0xFF index, which seems to be used for finished animations. std::cerr << "unknown animation group index: " << static_cast(anis.mGroupIndex) << std::endl; } } openmw-openmw-0.49.0/apps/essimporter/convertacdt.hpp000066400000000000000000000013121503074453300227610ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTACDT_H #define OPENMW_ESSIMPORT_CONVERTACDT_H #include #include #include #include #include "importacdt.hpp" 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 convertNpcData(const ActorData& actorData, ESM::NpcStats& npcStats); void convertANIS(const ANIS& anis, ESM::AnimationState& state); } #endif openmw-openmw-0.49.0/apps/essimporter/convertcntc.cpp000066400000000000000000000003431503074453300227730ustar00rootroot00000000000000#include "convertcntc.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertCNTC(const CNTC& cntc, ESM::ContainerState& state) { convertInventory(cntc.mInventory, state.mInventory); } } openmw-openmw-0.49.0/apps/essimporter/convertcntc.hpp000066400000000000000000000003771503074453300230070ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTCNTC_H #define OPENMW_ESSIMPORT_CONVERTCNTC_H #include "importcntc.hpp" #include namespace ESSImport { void convertCNTC(const CNTC& cntc, ESM::ContainerState& state); } #endif openmw-openmw-0.49.0/apps/essimporter/convertcrec.cpp000066400000000000000000000003421503074453300227570ustar00rootroot00000000000000#include "convertcrec.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertCREC(const CREC& crec, ESM::CreatureState& state) { convertInventory(crec.mInventory, state.mInventory); } } openmw-openmw-0.49.0/apps/essimporter/convertcrec.hpp000066400000000000000000000003751503074453300227720ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTCREC_H #define OPENMW_ESSIMPORT_CONVERTCREC_H #include "importcrec.hpp" #include namespace ESSImport { void convertCREC(const CREC& crec, ESM::CreatureState& state); } #endif openmw-openmw-0.49.0/apps/essimporter/converter.cpp000066400000000000000000000465521503074453300224660ustar00rootroot00000000000000#include "converter.hpp" #include #include #include #include #include #include #include #include "convertcntc.hpp" #include "convertcrec.hpp" #include "convertscri.hpp" namespace { void convertImage(char* data, int size, int width, int height, GLenum pf, const std::string& out) { osg::ref_ptr image(new osg::Image); image->allocateImage(width, height, 1, pf, GL_UNSIGNED_BYTE); memcpy(image->data(), data, size); image->flipVertical(); 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.mRef.mCount = 0; convertSCRI(cellref.mActorData.mSCRI, objstate.mLocals); objstate.mHasLocals = !objstate.mLocals.mVariables.empty(); if (cellref.mActorData.mHasANIS) convertANIS(cellref.mActorData.mANIS, objstate.mAnimationState); } bool isIndexedRefId(const std::string& indexedRefId) { if (indexedRefId.size() <= 8) return false; if (indexedRefId.find_first_not_of("0123456789") == std::string::npos) 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); 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 >> refIndex; refId = indexedRefId.substr(0, indexedRefId.size() - 8); } int convertActorId(const std::string& indexedRefId, ESSImport::Context& context) { if (isIndexedRefId(indexedRefId)) { int refIndex = 0; std::string refId; splitIndexedRefId(indexedRefId, 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; } else if (indexedRefId == "PlayerSaveGame") { return context.mPlayer.mObject.mCreatureStats.mActorId; } return -1; } } namespace ESSImport { struct MAPH { uint32_t size; uint32_t value; }; void ConvertFMAP::read(ESM::ESMReader& esm) { MAPH maph; esm.getHNT("MAPH", maph.size, maph.value); std::vector data; esm.getSubNameIs("MAPD"); esm.getSubHeader(); data.resize(esm.getSubSize()); 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.data(), data.size()); // to match openmw size // FIXME: filtering? mGlobalMapImage->scaleImage(maph.size * 2, maph.size * 2, 1, GL_UNSIGNED_BYTE); } 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; // 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; osg::ref_ptr image2(new osg::Image); int width = cellSize * numcells; int height = cellSize * numcells; std::vector data; data.resize(width * height * 4, 0); image2->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); memcpy(image2->data(), data.data(), data.size()); 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) { // 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; imageLeftSrc += exploredCell.first * cellSize; imageTopSrc -= exploredCell.second * cellSize; int imageLeftDst = width / 2; int imageTopDst = height / 2; imageLeftDst += exploredCell.first * cellSize; imageTopDst -= exploredCell.second * cellSize; for (int x = 0; x < cellSize; ++x) for (int y = 0; y < cellSize; ++y) { unsigned int col = *(unsigned int*)mGlobalMapImage->data(imageLeftSrc + x, imageTopSrc + y, 0); *(unsigned int*)image2->data(imageLeftDst + x, imageTopDst + y, 0) = col; } } std::stringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { std::cerr << "Error: can't write global map image, no png readerwriter found" << std::endl; return; } image2->flipVertical(); 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; return; } std::string outData = ostream.str(); mContext->mGlobalMapState.mImageData = std::vector(outData.begin(), outData.end()); esm.startRecord(ESM::REC_GMAP); mContext->mGlobalMapState.save(esm); esm.endRecord(ESM::REC_GMAP); } void ConvertCell::read(ESM::ESMReader& esm) { ESM::Cell cell; bool isDeleted = false; cell.load(esm, isDeleted, false); // I wonder what 0x40 does? if (cell.isExterior() && cell.mData.mFlags & 0x20) { mContext->mGlobalMapState.mMarkers.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); } // note if the player is in a nameless exterior cell, we will assign the cellId later based on player position if (Misc::StringUtils::ciEqual(cell.mName, mContext->mPlayerCellName)) { mContext->mPlayer.mCellId = cell.mId; } Cell newcell; newcell.mCell = cell; // fog of war // seems to be a 1-bit pixel format, 16*16 pixels // TODO: add bleeding of FOW into neighbouring cells (openmw handles this by writing to the textures, // MW handles it when rendering only) unsigned char nam8[32]; // exterior has 1 NAM8, interior can have multiple ones, and have an extra 4 byte flag at the start // (probably offset of that specific fog texture?) while (esm.isNextSub("NAM8")) { if (cell.isExterior()) // TODO: NAM8 occasionally exists for cells that haven't been explored. // are there any flags marking explored cells? mContext->mExploredCells.insert(std::make_pair(cell.mData.mX, cell.mData.mY)); esm.getSubHeader(); if (esm.getSubSize() == 36) { // flag on interiors esm.skip(4); } esm.getT(nam8); newcell.mFogOfWar.reserve(16 * 16); for (int x = 0; x < 16; ++x) { 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; newcell.mFogOfWar.push_back(((nam8[bytepos] >> bit) & (0x1)) ? 0xffffffff : 0x000000ff); } } if (cell.isExterior()) { 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()); } } // moved reference, not handled yet // NOTE: MVRF can also occur in within normal references (importcellref.cpp)? // this does not match the ESM file implementation, // verify if that can happen with ESM files too while (esm.isNextSub("MVRF")) { esm.skipHSub(); // skip MVRF esm.getSubName(); esm.skipHSub(); // skip CNDT } std::vector cellrefs; while (esm.hasMoreSubs() && esm.peekNextSub("FRMR")) { CellRef ref; ref.load(esm); cellrefs.push_back(ref); } while (esm.isNextSub("MPCD")) { float notepos[3]; 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, // i.e. when the grid is exceeded. // Converting the interior markers correctly could be rather tricky, but is probably similar logic // as used for the FoW texture placement, which we need to figure out anyway notepos[1] += 31.f; notepos[0] += 0.5; notepos[1] += 0.5; notepos[0] = Constants::CellSizeInUnits * notepos[0] / 32.f; notepos[1] = Constants::CellSizeInUnits * notepos[1] / 32.f; if (cell.isExterior()) { notepos[0] += Constants::CellSizeInUnits * cell.mData.mX; notepos[1] += Constants::CellSizeInUnits * cell.mData.mY; } // TODO: what encoding is this in? std::string note = esm.getHNString("MPNT"); ESM::CustomMarker marker; marker.mWorldX = notepos[0]; marker.mWorldY = notepos[1]; marker.mNote = std::move(note); marker.mCell = cell.mId; mMarkers.push_back(marker); } newcell.mRefs = std::move(cellrefs); if (cell.isExterior()) mExtCells[std::make_pair(cell.mData.mX, cell.mData.mY)] = std::move(newcell); else mIntCells[cell.mName] = std::move(newcell); } 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.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) { ESM::CellRef out(cellref); // TODO: use mContext->mCreatures/mNpcs if (!isIndexedRefId(cellref.mIndexedRefId)) { // 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 = ESM::RefId::stringRefId(cellref.mIndexedRefId); ESM::ObjectState objstate; objstate.blank(); objstate.mRef = out; objstate.mRef.mRefID = out.mRefID; objstate.mHasCustomState = false; convertCellRef(cellref, objstate); esm.writeHNT("OBJE", 0); objstate.save(esm); continue; } else { int refIndex = 0; std::string outStringId; splitIndexedRefId(cellref.mIndexedRefId, refIndex, outStringId); out.mRefID = ESM::RefId::stringRefId(outStringId); 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 = out.mRefID; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values 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)); esm.writeHNT("OBJE", ESM::REC_NPC_); objstate.save(esm); continue; } 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 = out.mRefID; convertCNTC(cntcIt->second, objstate); convertCellRef(cellref, objstate); esm.writeHNT("OBJE", ESM::REC_CONT); objstate.save(esm); continue; } 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 = out.mRefID; // TODO: need more micromanagement here so we don't overwrite values // from the ESM with default values 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)); esm.writeHNT("OBJE", ESM::REC_CREA); objstate.save(esm); continue; } std::stringstream error; error << "Can't find type for " << cellref.mIndexedRefId << std::endl; throw std::runtime_error(error.str()); } } esm.endRecord(ESM::REC_CSTA); } void ConvertCell::write(ESM::ESMWriter& esm) { for (const auto& cell : mIntCells) writeCell(cell.second, esm); for (const auto& cell : mExtCells) writeCell(cell.second, esm); for (const auto& marker : mMarkers) { esm.startRecord(ESM::REC_MARK); marker.save(esm); esm.endRecord(ESM::REC_MARK); } } void ConvertPROJ::read(ESM::ESMReader& esm) { mProj.load(esm); } void ConvertPROJ::write(ESM::ESMWriter& esm) { for (const PROJ::PNAM& pnam : mProj.mProjectiles) { if (!pnam.isMagic()) { ESM::ProjectileState out; convertBaseState(out, pnam); out.mBowId = ESM::RefId::stringRefId(pnam.mBowId.toString()); out.mVelocity = pnam.mVelocity; out.mAttackStrength = pnam.mAttackStrength; esm.startRecord(ESM::REC_PROJ); out.save(esm); esm.endRecord(ESM::REC_PROJ); } else { ESM::MagicBoltState out; 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; }); if (it == mContext->mActiveSpells.end()) { std::cerr << "Warning: Skipped conversion for magic projectile \"" << pnam.mArrowId.toString() << "\" (invalid spell link)" << std::endl; continue; } 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); esm.endRecord(ESM::REC_MPRJ); } } } void ConvertPROJ::convertBaseState(ESM::BaseProjectileState& base, const PROJ::PNAM& pnam) { base.mId = ESM::RefId::stringRefId(pnam.mArrowId.toString()); base.mPosition = pnam.mPosition; osg::Quat orient; orient.makeRotate(osg::Vec3f(0, 1, 0), pnam.mVelocity); base.mOrientation = orient; base.mActorId = convertActorId(pnam.mActorId.toString(), *mContext); } void ConvertSPLM::read(ESM::ESMReader& esm) { mSPLM.load(esm); mContext->mActiveSpells = mSPLM.mActiveSpells; } void ConvertSPLM::write(ESM::ESMWriter& esm) { std::cerr << "Warning: Skipped active spell conversion (not implemented)" << std::endl; } } openmw-openmw-0.49.0/apps/essimporter/converter.hpp000066400000000000000000000456431503074453300224730ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTER_H #define OPENMW_ESSIMPORT_CONVERTER_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "importcntc.hpp" #include "importcrec.hpp" #include "importcellref.hpp" #include "importdial.hpp" #include "importercontext.hpp" #include "importgame.hpp" #include "importinfo.hpp" #include "importjour.hpp" #include "importklst.hpp" #include "importproj.h" #include "importques.hpp" #include "importscpt.hpp" #include "importsplm.h" #include "convertacdt.hpp" #include "convertnpcc.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() = default; 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) {} /// 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) {} protected: Context* mContext; }; /// Default converter: simply reads the record and writes it unmodified to the output template class DefaultConverter : public Converter { public: int getStage() override { return 0; } void read(ESM::ESMReader& esm) override { T record; bool isDeleted = false; record.load(esm, isDeleted); mRecords[record.mId] = record; } void write(ESM::ESMWriter& esm) override { 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; }; class ConvertNPC : public Converter { public: void read(ESM::ESMReader& esm) override { 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 { // See comment in ConvertNPC ESM::Creature creature; bool isDeleted = false; 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... class ConvertGlobal : public DefaultConverter { 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 { 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; mRecords[class_.mId] = class_; } }; class ConvertBook : public DefaultConverter { 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(book.mId); mRecords[book.mId] = book; } }; class ConvertNPCC : public Converter { public: void read(ESM::ESMReader& esm) override { 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 { 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::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 (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 == refr.mActorData.mSelectedEnchantItem) invState.mSelectedEnchantItem = i; } } } void write(ESM::ESMWriter& esm) override { esm.startRecord(ESM::REC_ASPL); esm.writeHNRefId("ID__", mSelectedSpell); esm.endRecord(ESM::REC_ASPL); } private: ESM::RefId mSelectedSpell; }; class ConvertPCDT : public Converter { public: ConvertPCDT() : mFirstPersonCam(true) , mTeleportingEnabled(true) , mLevitationEnabled(true) { } void read(ESM::ESMReader& esm) override { PCDT pcdt; pcdt.load(esm); 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 { auto id = esm.getHNRefId("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 { auto id = esm.getHNRefId("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; private: osg::ref_ptr mGlobalMapImage; }; class ConvertCell : public Converter { public: void read(ESM::ESMReader& esm) override; void write(ESM::ESMWriter& esm) override; private: struct Cell { ESM::Cell mCell; std::vector mRefs; std::vector mFogOfWar; }; std::map mIntCells; std::map, Cell> mExtCells; std::vector mMarkers; void writeCell(const Cell& cell, ESM::ESMWriter& esm); }; class ConvertKLST : public Converter { public: void read(ESM::ESMReader& esm) override { KLST klst; klst.load(esm); mKillCounter = klst.mKillCounter; mContext->mPlayer.mObject.mNpcStats.mWerewolfKills = klst.mWerewolfKills; } void write(ESM::ESMWriter& esm) override { esm.startRecord(ESM::REC_DCOU); for (const auto& [id, count] : mKillCounter) { esm.writeHNRefId("ID__", id); esm.writeHNT("COUN", count); } esm.endRecord(ESM::REC_DCOU); } private: std::map mKillCounter; }; class ConvertFACT : public Converter { public: void read(ESM::ESMReader& esm) override { 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) { const auto& faction2 = it->first; mContext->mDialogueState.mChangedFactionReaction[id].insert(std::make_pair(faction2, it->second)); } } }; /// 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")) { 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 (auto it = mStolenItems.begin(); it != mStolenItems.end(); ++it) { 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)); } esm.startRecord(ESM::REC_STLN); items.write(esm); esm.endRecord(ESM::REC_STLN); } private: typedef std::pair Owner; // 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 { 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 { 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); } }; class ConvertJOUR : public Converter { public: void read(ESM::ESMReader& esm) override { JOUR journal; journal.load(esm); } }; class ConvertGAME : public Converter { public: ConvertGAME() : mHasGame(false) { } void read(ESM::ESMReader& esm) override { mGame.load(esm); mHasGame = true; } int validateWeatherID(int weatherID) { 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 { 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; }; } #endif openmw-openmw-0.49.0/apps/essimporter/convertinventory.cpp000066400000000000000000000020021503074453300240730ustar00rootroot00000000000000#include "convertinventory.hpp" #include #include namespace ESSImport { void convertInventory(const Inventory& inventory, ESM::InventoryState& state) { uint32_t index = 0; for (const auto& item : inventory.mItems) { ESM::ObjectState objstate; objstate.blank(); objstate.mRef = item; 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 // Not a big deal, OpenMW will auto-correct to a valid slot, the only problem is when // an item could be equipped in two different slots (e.g. equipped two rings) state.mEquipmentSlots[index] = item.mRelativeEquipmentSlot; ++index; } } } openmw-openmw-0.49.0/apps/essimporter/convertinventory.hpp000066400000000000000000000004351503074453300241100ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTINVENTORY_H #define OPENMW_ESSIMPORT_CONVERTINVENTORY_H #include "importinventory.hpp" #include namespace ESSImport { void convertInventory(const Inventory& inventory, ESM::InventoryState& state); } #endif openmw-openmw-0.49.0/apps/essimporter/convertnpcc.cpp000066400000000000000000000005471503074453300227750ustar00rootroot00000000000000#include "convertnpcc.hpp" #include "convertinventory.hpp" namespace ESSImport { void convertNPCC(const NPCC& npcc, ESM::NpcState& npcState) { npcState.mNpcStats.mDisposition = npcc.mNPDT.mDisposition; npcState.mNpcStats.mReputation = npcc.mNPDT.mReputation; convertInventory(npcc.mInventory, npcState.mInventory); } } openmw-openmw-0.49.0/apps/essimporter/convertnpcc.hpp000066400000000000000000000003661503074453300230010ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTNPCC_H #define OPENMW_ESSIMPORT_CONVERTNPCC_H #include "importnpcc.hpp" #include namespace ESSImport { void convertNPCC(const NPCC& npcc, ESM::NpcState& npcState); } #endif openmw-openmw-0.49.0/apps/essimporter/convertplayer.cpp000066400000000000000000000100531503074453300233370ustar00rootroot00000000000000#include "convertplayer.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) { 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) { ESM::NpcStats::Faction faction; faction.mExpelled = (essFaction.mFlags & 0x2) != 0; faction.mRank = essFaction.mRank; faction.mReputation = essFaction.mReputation; out.mObject.mNpcStats.mFactions[ESM::RefId::stringRefId(essFaction.mFactionName.toString())] = faction; } for (size_t i = 0; i < out.mObject.mNpcStats.mSpecIncreases.size(); ++i) out.mObject.mNpcStats.mSpecIncreases[i] = pcdt.mPNAM.mSpecIncreases[i]; for (size_t i = 0; i < out.mObject.mNpcStats.mSkillIncrease.size(); ++i) out.mObject.mNpcStats.mSkillIncrease[i] = pcdt.mPNAM.mSkillIncreases[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; if (pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_WeaponDrawn) out.mObject.mCreatureStats.mDrawState = 1; if (pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_SpellDrawn) out.mObject.mCreatureStats.mDrawState = 2; firstPersonCam = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ThirdPerson); teleportingEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_TeleportingDisabled); levitationEnabled = !(pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_LevitationDisabled); for (const auto& knownDialogueTopic : pcdt.mKnownDialogueTopics) { outDialogueTopics.push_back(ESM::RefId::stringRefId(knownDialogueTopic)); } controls.mViewSwitchDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ViewSwitchDisabled; controls.mControlsDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_ControlsDisabled; controls.mJumpingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_JumpingDisabled; controls.mLookingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_LookingDisabled; controls.mVanityModeDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_VanityModeDisabled; controls.mWeaponDrawingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_WeaponDrawingDisabled; controls.mSpellDrawingDisabled = pcdt.mPNAM.mPlayerFlags & PCDT::PlayerFlags_SpellDrawingDisabled; if (pcdt.mHasMark) { out.mHasMark = 1; const PCDT::PNAM::MarkLocation& mark = pcdt.mPNAM.mMarkLocation; // TODO: Figure out a better way to detect interiors. (0, 0) is a valid exterior cell. 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; out.mMarkedPosition.pos[1] = mark.mY; out.mMarkedPosition.pos[2] = mark.mZ; out.mMarkedPosition.rot[0] = out.mMarkedPosition.rot[1] = 0.0f; out.mMarkedPosition.rot[2] = mark.mRotZ; } if (pcdt.mHasENAM) { out.mLastKnownExteriorPosition[0] = (pcdt.mENAM.mCellX + 0.5f) * Constants::CellSizeInUnits; out.mLastKnownExteriorPosition[1] = (pcdt.mENAM.mCellY + 0.5f) * Constants::CellSizeInUnits; out.mLastKnownExteriorPosition[2] = 0.0f; } } } openmw-openmw-0.49.0/apps/essimporter/convertplayer.hpp000066400000000000000000000006731503074453300233530ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTPLAYER_H #define OPENMW_ESSIMPORT_CONVERTPLAYER_H #include "importplayer.hpp" #include #include namespace ESSImport { void convertPCDT(const PCDT& pcdt, ESM::Player& out, std::vector& outDialogueTopics, bool& firstPersonCam, bool& teleportingEnabled, bool& levitationEnabled, ESM::ControlsState& controls); } #endif openmw-openmw-0.49.0/apps/essimporter/convertscpt.cpp000066400000000000000000000007021503074453300230140ustar00rootroot00000000000000#include "convertscpt.hpp" #include "convertscri.hpp" #include namespace ESSImport { void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out) { out.mId = ESM::RefId::stringRefId(scpt.mSCHD.mName.toString()); out.mRunning = scpt.mRunning; out.mTargetRef = ESM::RefNum{}; // TODO: convert target reference of global script convertSCRI(scpt.mSCRI, out.mLocals); } } openmw-openmw-0.49.0/apps/essimporter/convertscpt.hpp000066400000000000000000000003711503074453300230230ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTSCPT_H #define OPENMW_ESSIMPORT_CONVERTSCPT_H #include #include "importscpt.hpp" namespace ESSImport { void convertSCPT(const SCPT& scpt, ESM::GlobalScript& out); } #endif openmw-openmw-0.49.0/apps/essimporter/convertscri.cpp000066400000000000000000000015311503074453300230040ustar00rootroot00000000000000#include "convertscri.hpp" namespace { template void storeVariables(const std::vector& variables, ESM::Locals& locals, const std::string& scriptname) { for (const auto& variable : variables) { ESM::Variant val(variable); val.setType(VariantType); locals.mVariables.emplace_back(std::string(), val); } } } namespace ESSImport { 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); } } openmw-openmw-0.49.0/apps/essimporter/convertscri.hpp000066400000000000000000000004341503074453300230120ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONVERTSCRI_H #define OPENMW_ESSIMPORT_CONVERTSCRI_H #include "importscri.hpp" #include namespace ESSImport { /// Convert script variable assignments void convertSCRI(const SCRI& scri, ESM::Locals& locals); } #endif openmw-openmw-0.49.0/apps/essimporter/importacdt.hpp000066400000000000000000000042161503074453300226210ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_ACDT_H #define OPENMW_ESSIMPORT_ACDT_H #include #include #include "importscri.hpp" namespace ESM { class ESMReader; } namespace ESSImport { enum ACDTFlags { TalkedToPlayer = 0x4, Attacked = 0x100, Unknown = 0x200 }; enum ACSCFlags { Dead = 0x2 }; /// Actor data, shared by (at least) REFR and CellRef 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]; 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 unsigned char mUnknown4[4]; uint32_t mGoldPool; unsigned char mCountDown; // seen the same value as in ACSC.mCorpseClearCountdown, maybe // this one is for respawning? unsigned char mUnknown5[3]; }; struct ACSC { unsigned char mUnknown1[17]; unsigned char mFlags; // ACSCFlags unsigned char mUnknown2[22]; unsigned char mCorpseClearCountdown; // hours? unsigned char mUnknown3[71]; }; struct ANIS { unsigned char mGroupIndex; unsigned char mUnknown[3]; float mTime; }; struct ActorData { bool mHasACDT; ACDT mACDT; bool mHasACSC; ACSC mACSC; int mSkills[27][2]; // skills, base and modified // creature combat stats, base and modified // I think these can be ignored in the conversion, because it is not possible // to change them ingame int mCombatStats[3][2]; ESM::RefId mSelectedSpell; ESM::RefId mSelectedEnchantItem; SCRI mSCRI; bool mHasANIS; ANIS mANIS; // scripted animation state }; } #endif openmw-openmw-0.49.0/apps/essimporter/importcellref.cpp000066400000000000000000000131421503074453300233130ustar00rootroot00000000000000#include "importcellref.hpp" #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); } template T> void decompose(T&& v, const auto& f) { f(v.mGroupIndex, v.mUnknown, v.mTime); } void CellRef::load(ESM::ESMReader& esm) { blank(); 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.mIndex &= 0x00ffffff; mIndexedRefId = esm.getHNString("NAME"); 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")) { ESM::RefId spellRefId = esm.getRefId(); if (esm.isNextSub("XNAM")) mActorData.mSelectedEnchantItem = esm.getRefId(); else mActorData.mSelectedSpell = std::move(spellRefId); 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; } mEnabled = true; esm.getHNOT(mEnabled, "ZNAM"); // 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 for (int i = 0; i < 2; ++i) esm.getOptionalComposite("DATA", mPos); mDeleted = 0; if (esm.isNextSub("DELE")) { uint32_t deleted; esm.getHT(deleted); mDeleted = ((deleted >> 24) & 0x2) != 0; // the other 3 bytes seem to be uninitialized garbage } if (esm.isNextSub("MVRF")) { esm.skipHSub(); esm.getSubName(); esm.skipHSub(); } } } openmw-openmw-0.49.0/apps/essimporter/importcellref.hpp000066400000000000000000000007651503074453300233270ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CELLREF_H #define OPENMW_ESSIMPORT_CELLREF_H #include #include #include "importacdt.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct CellRef : public ESM::CellRef { std::string mIndexedRefId; std::string mScript; bool mEnabled; bool mDeleted; ActorData mActorData; void load(ESM::ESMReader& esm); ~CellRef() = default; }; } #endif openmw-openmw-0.49.0/apps/essimporter/importcntc.cpp000066400000000000000000000003761503074453300226330ustar00rootroot00000000000000#include "importcntc.hpp" #include #include namespace ESSImport { void CNTC::load(ESM::ESMReader& esm) { mIndex = 0; esm.getHNT(mIndex, "INDX"); mInventory.load(esm); } } openmw-openmw-0.49.0/apps/essimporter/importcntc.hpp000066400000000000000000000005261503074453300226350ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTCNTC_H #define OPENMW_ESSIMPORT_IMPORTCNTC_H #include "importinventory.hpp" namespace ESM { class ESMReader; } namespace ESSImport { /// Changed container contents struct CNTC { int32_t mIndex; Inventory mInventory; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importcrec.cpp000066400000000000000000000011101503074453300226030ustar00rootroot00000000000000#include "importcrec.hpp" #include namespace ESSImport { void CREC::load(ESM::ESMReader& esm) { esm.getHNT(mIndex, "INDX"); // equivalent of ESM::Creature XSCL? probably don't have to convert this, // since the value can't be changed 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")) mAiPackages.add(esm); mInventory.load(esm); } } openmw-openmw-0.49.0/apps/essimporter/importcrec.hpp000066400000000000000000000006451503074453300226240ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CREC_H #define OPENMW_ESSIMPORT_CREC_H #include "importinventory.hpp" #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// Creature changes struct CREC { int32_t mIndex; Inventory mInventory; ESM::AIPackageList mAiPackages; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importdial.cpp000066400000000000000000000011301503074453300226020ustar00rootroot00000000000000#include "importdial.hpp" #include namespace ESSImport { void DIAL::load(ESM::ESMReader& esm) { // See ESM::Dialogue::Type enum, not sure why we would need this here though int32_t type = 0; esm.getHNOT(type, "DATA"); // Deleted dialogue in a savefile. No clue what this means... int32_t deleted = 0; esm.getHNOT(deleted, "DELE"); mIndex = 0; // *should* always occur except when the dialogue is deleted, but leaving it optional just in case... esm.getHNOT(mIndex, "XIDX"); } } openmw-openmw-0.49.0/apps/essimporter/importdial.hpp000066400000000000000000000004321503074453300226130ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTDIAL_H #define OPENMW_ESSIMPORT_IMPORTDIAL_H #include namespace ESM { class ESMReader; } namespace ESSImport { struct DIAL { int32_t mIndex; // Journal index void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importer.cpp000066400000000000000000000404341503074453300223110ustar00rootroot00000000000000#include "importer.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "importercontext.hpp" #include "converter.hpp" namespace { void writeScreenshot(const ESM::Header& fileHeader, ESM::SavedGame& out) { if (fileHeader.mSCRS.size() != 128 * 128 * 4) { std::cerr << "Error: unexpected screenshot size " << std::endl; return; } 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 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++; ++it; // skip alpha } } image->flipVertical(); std::stringstream ostream; osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { std::cerr << "Error: can't write screenshot: no jpg readerwriter found" << std::endl; return; } 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; return; } std::string data = ostream.str(); out.mScreenshot = std::vector(data.begin(), data.end()); } } namespace ESSImport { Importer::Importer( const std::filesystem::path& essfile, const std::filesystem::path& outfile, const std::string& encoding) : mEssFile(essfile) , mOutFile(outfile) , mEncoding(encoding) { } struct File { struct Subrecord { std::string mName; size_t mFileOffset; std::vector mData; }; struct Record { std::string mName; size_t mFileOffset; std::vector mSubrecords; }; std::vector mRecords; }; void read(const std::filesystem::path& filename, File& file) { ESM::ESMReader esm; esm.open(filename); while (esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); File::Record rec; rec.mName = n.toString(); rec.mFileOffset = esm.getFileOffset(); while (esm.hasMoreSubs()) { File::Subrecord sub; esm.getSubName(); esm.getSubHeader(); sub.mFileOffset = esm.getFileOffset(); sub.mName = esm.retSubName().toString(); sub.mData.resize(esm.getSubSize()); esm.getExact(sub.mData.data(), sub.mData.size()); rec.mSubrecords.push_back(sub); } file.mRecords.push_back(rec); } } void Importer::compare() { // data that always changes (and/or is already fully decoded) should be blacklisted 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? blacklist.insert(std::make_pair("CELL", "ND3D")); blacklist.insert(std::make_pair("REFR", "ND3D")); File file1; read(mEssFile, file1); File file2; read(mOutFile, file2); // todo rename variable // FIXME: use max(size1, size2) for (size_t i = 0; i < file1.mRecords.size(); ++i) { File::Record rec = file1.mRecords[i]; if (i >= file2.mRecords.size()) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Record in file1 not present in file2: (1) 0x" << std::hex << rec.mFileOffset << std::endl; std::cout.flags(f); return; } File::Record rec2 = file2.mRecords[i]; if (rec.mName != rec2.mName) { std::ios::fmtflags f(std::cout.flags()); std::cout << "Different record name at (2) 0x" << std::hex << rec2.mFileOffset << std::endl; std::cout.flags(f); return; // TODO: try to recover } // FIXME: use max(size1, size2) for (size_t j = 0; j < rec.mSubrecords.size(); ++j) { File::Subrecord sub = rec.mSubrecords[j]; if (j >= 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.flags(f); return; } File::Subrecord sub2 = rec2.mSubrecords[j]; 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.flags(f); break; // TODO: try to recover } if (sub.mData != sub2.mData) { if (blacklist.find(std::make_pair(rec.mName, sub.mName)) != blacklist.end()) continue; 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 << "Data 1:" << std::endl; for (size_t k = 0; k < sub.mData.size(); ++k) { bool different = false; if (k >= sub2.mData.size() || sub2.mData[k] != sub.mData[k]) different = true; if (different) std::cout << "\033[033m"; std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub.mData[k] << " "; if (different) std::cout << "\033[0m"; } std::cout << std::endl; std::cout << "Data 2:" << std::endl; for (size_t k = 0; k < sub2.mData.size(); ++k) { bool different = false; if (k >= sub.mData.size() || sub.mData[k] != sub2.mData[k]) different = true; if (different) std::cout << "\033[033m"; std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)sub2.mData[k] << " "; if (different) std::cout << "\033[0m"; } std::cout << std::endl; std::cout.flags(f); } } } } void Importer::run() { ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncoding)); ESM::ESMReader esm; esm.open(mEssFile); esm.setEncoder(&encoder); Context context; const ESM::Header& header = esm.getHeader(); context.mPlayerCellName = header.mGameData.mCurrentCell.toString(); 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?) // - VFXM // - SPLM (active spell effects) std::set unknownRecords; for (const auto& converter : converters) { converter.second->setContext(context); } while (esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); auto it = converters.find(n.toInt()); if (it != converters.end()) { it->second->read(esm); } else { 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.flags(f); } esm.skipRecord(); } } ESM::ESMWriter writer; writer.setFormatVersion(ESM::CurrentSaveGameFormatVersion); 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); for (const auto& master : header.mMaster) writer.addMaster(master.name, 0); // not using the size information anyway -> use value of 0 writer.save(stream); ESM::SavedGame profile; for (const auto& master : header.mMaster) { profile.mContentFiles.push_back(master.name); } profile.mDescription = esm.getDesc(); profile.mInGameTime.mDay = context.mDay; profile.mInGameTime.mGameHour = context.mHour; profile.mInGameTime.mMonth = context.mMonth; profile.mInGameTime.mYear = context.mYear; profile.mTimePlayed = 0; profile.mPlayerCellName = context.mPlayerCellName; if (context.mPlayerBase.mClass == "NEWCLASSID_CHARGEN") profile.mPlayerClassName = context.mCustomPlayerClassName; else profile.mPlayerClassId = context.mPlayerBase.mClass; profile.mPlayerLevel = context.mPlayerBase.mNpdt.mLevel; profile.mPlayerName = header.mGameData.mPlayerName.toString(); writeScreenshot(header, profile); 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 (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 0) continue; it->second->write(writer); } writer.startRecord(ESM::REC_NPC_); context.mPlayerBase.mId = ESM::RefId::stringRefId("Player"); context.mPlayerBase.save(writer); writer.endRecord(ESM::REC_NPC_); for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 1) continue; it->second->write(writer); } writer.startRecord(ESM::REC_PLAY); 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); writer.startRecord(ESM::REC_ACTC); writer.writeHNT("COUN", context.mNextActorId); writer.endRecord(ESM::REC_ACTC); // Stage 2 requires cell references to be written / actors IDs assigned for (auto it = converters.begin(); it != converters.end(); ++it) { if (it->second->getStage() != 2) continue; it->second->write(writer); } writer.startRecord(ESM::REC_DIAS); context.mDialogueState.save(writer); writer.endRecord(ESM::REC_DIAS); writer.startRecord(ESM::REC_INPU); context.mControlsState.save(writer); writer.endRecord(ESM::REC_INPU); } } openmw-openmw-0.49.0/apps/essimporter/importer.hpp000066400000000000000000000007451503074453300223170ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORTER_IMPORTER_H #define OPENMW_ESSIMPORTER_IMPORTER_H #include namespace ESSImport { class Importer { public: Importer( const std::filesystem::path& essfile, const std::filesystem::path& outfile, const std::string& encoding); void run(); void compare(); private: std::filesystem::path mEssFile; std::filesystem::path mOutFile; std::string mEncoding; }; } #endif openmw-openmw-0.49.0/apps/essimporter/importercontext.cpp000066400000000000000000000000001503074453300236770ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/essimporter/importercontext.hpp000066400000000000000000000050211503074453300237140ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_CONTEXT_H #define OPENMW_ESSIMPORT_CONTEXT_H #include #include #include #include #include #include #include #include #include "importcntc.hpp" #include "importcrec.hpp" #include "importnpcc.hpp" #include "importsplm.h" namespace ESSImport { struct Context { // set from the TES3 header std::string mPlayerCellName; ESM::Player mPlayer; ESM::NPC mPlayerBase; std::string mCustomPlayerClassName; ESM::DialogueState mDialogueState; ESM::ControlsState mControlsState; // cells which should show an explored overlay on the global map std::set> mExploredCells; ESM::GlobalMap mGlobalMapState; int mDay, mMonth, mYear; float mHour; // key std::map, CREC> mCreatureChanges; std::map, NPCC> mNpcChanges; std::map, CNTC> mContainerChanges; std::map, int> mActorIdMap; int mNextActorId; std::map mCreatures; std::map mNpcs; std::vector mActiveSpells; Context() : mDay(0) , mMonth(0) , mYear(0) , mHour(0.f) , mNextActorId(0) { 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 = ESM::RefId::stringRefId("player"); // REFR.mRefID would be PlayerSaveGame mPlayer.mObject.mCreatureStats.mActorId = generateActorId(); mGlobalMapState.mBounds.mMinX = 0; mGlobalMapState.mBounds.mMaxX = 0; mGlobalMapState.mBounds.mMinY = 0; mGlobalMapState.mBounds.mMaxY = 0; mPlayerBase.blank(); } int generateActorId() { return mNextActorId++; } }; } #endif openmw-openmw-0.49.0/apps/essimporter/importgame.cpp000066400000000000000000000014311503074453300226060ustar00rootroot00000000000000#include "importgame.hpp" #include namespace ESSImport { void GAME::load(ESM::ESMReader& esm) { 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); } } openmw-openmw-0.49.0/apps/essimporter/importgame.hpp000066400000000000000000000014221503074453300226130ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_GAME_H #define OPENMW_ESSIMPORT_GAME_H #include namespace ESM { class ESMReader; } namespace ESSImport { /// Weather data struct GAME { struct GMDT { 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; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importinfo.cpp000066400000000000000000000012211503074453300226250ustar00rootroot00000000000000#include "importinfo.hpp" #include namespace ESSImport { void INFO::load(ESM::ESMReader& esm) { if (esm.peekNextSub("XNAM")) { // TODO: Support older saves by turning XNAM into a RefId. // XNAM is probably the number of the topic response within the topic record's linked list. // Resolving this value will likely require loading Morrowind.esm. esm.getSubName(); esm.skipHSub(); mInfo = ESM::RefId(); } else mInfo = esm.getHNRefId("INAM"); mActorRefId = esm.getHNString("ACDT"); } } openmw-openmw-0.49.0/apps/essimporter/importinfo.hpp000066400000000000000000000005201503074453300226330ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTINFO_H #define OPENMW_ESSIMPORT_IMPORTINFO_H #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct INFO { ESM::RefId mInfo; std::string mActorRefId; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importinventory.cpp000066400000000000000000000044531503074453300237410ustar00rootroot00000000000000#include "importinventory.hpp" #include #include namespace ESSImport { void Inventory::load(ESM::ESMReader& esm) { while (esm.isNextSub("NPCO")) { ContItem 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.mChargeIntRemainder = 0; unsigned int itemCount = std::abs(item.mCount); bool separateStacks = false; for (unsigned int i = 0; i < itemCount; ++i) { bool newStack = esm.isNextSub("XIDX"); if (newStack) { uint32_t idx; esm.getHT(idx); separateStacks = true; item.mCount = 1; } item.mSCRI.load(esm); // for XSOL and XCHG seen so far, but probably others too bool isDeleted = false; item.ESM::CellRef::loadData(esm, isDeleted); int32_t charge = -1; esm.getHNOT(charge, "XHLT"); item.mChargeInt = charge; if (newStack) mItems.push_back(item); } if (!separateStacks) mItems.push_back(item); } // equipped items while (esm.isNextSub("WIDX")) { // note: same item can be equipped 2 items (e.g. 2 rings) // and will be *stacked* in the NPCO list, unlike openmw! // this is currently not handled properly. esm.getSubHeader(); int32_t itemIndex; // index of the item in the NPCO list esm.getT(itemIndex); if (itemIndex < 0 || itemIndex >= int(mItems.size())) esm.fail("equipment item index out of range"); // appears to be a relative index for only the *possible* slots this item can be equipped in, // i.e. 0 most of the time int32_t slotIndex; esm.getT(slotIndex); mItems[itemIndex].mRelativeEquipmentSlot = slotIndex; } } } openmw-openmw-0.49.0/apps/essimporter/importinventory.hpp000066400000000000000000000013241503074453300237400ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTINVENTORY_H #define OPENMW_ESSIMPORT_IMPORTINVENTORY_H #include #include #include #include #include #include "importscri.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct ContItem { int32_t mCount; ESM::NAME32 mItem; }; struct Inventory { struct InventoryItem : public ESM::CellRef { std::string mId; int32_t mCount; int32_t mRelativeEquipmentSlot; SCRI mSCRI; }; std::vector mItems; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importjour.cpp000066400000000000000000000002751503074453300226610ustar00rootroot00000000000000#include "importjour.hpp" #include namespace ESSImport { void JOUR::load(ESM::ESMReader& esm) { mText = esm.getHNString("NAME"); } } openmw-openmw-0.49.0/apps/essimporter/importjour.hpp000066400000000000000000000005021503074453300226570ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTJOUR_H #define OPENMW_ESSIMPORT_IMPORTJOUR_H #include namespace ESM { class ESMReader; } namespace ESSImport { /// Journal struct JOUR { // The entire journal, in HTML std::string mText; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importklst.cpp000066400000000000000000000006621503074453300226570ustar00rootroot00000000000000#include "importklst.hpp" #include namespace ESSImport { void KLST::load(ESM::ESMReader& esm) { while (esm.isNextSub("KNAM")) { ESM::RefId refId = esm.getRefId(); int32_t count; esm.getHNT(count, "CNAM"); mKillCounter[refId] = count; } mWerewolfKills = 0; esm.getHNOT(mWerewolfKills, "INTV"); } } openmw-openmw-0.49.0/apps/essimporter/importklst.hpp000066400000000000000000000006011503074453300226550ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_KLST_H #define OPENMW_ESSIMPORT_KLST_H #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// Kill Stats struct KLST { void load(ESM::ESMReader& esm); std::map mKillCounter; int32_t mWerewolfKills; }; } #endif openmw-openmw-0.49.0/apps/essimporter/importnpcc.cpp000066400000000000000000000007341503074453300226250ustar00rootroot00000000000000#include "importnpcc.hpp" #include namespace ESSImport { void NPCC::load(ESM::ESMReader& esm) { 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")) mAiPackages.add(esm); mInventory.load(esm); } } openmw-openmw-0.49.0/apps/essimporter/importnpcc.hpp000066400000000000000000000011261503074453300226260ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_NPCC_H #define OPENMW_ESSIMPORT_NPCC_H #include #include #include "importinventory.hpp" namespace ESM { class ESMReader; } namespace ESSImport { struct NPCC { struct NPDT { unsigned char mDisposition; unsigned char unknown; unsigned char mReputation; unsigned char unknown2; int32_t mIndex; } mNPDT; Inventory mInventory; ESM::AIPackageList mAiPackages; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importplayer.cpp000066400000000000000000000053641503074453300232020ustar00rootroot00000000000000#include "importplayer.hpp" #include namespace ESSImport { void PCDT::load(ESM::ESMReader& esm) { while (esm.isNextSub("DNAM")) { mKnownDialogueTopics.push_back(esm.getHString()); } mHasMark = false; if (esm.isNextSub("MNAM")) { mHasMark = true; mMNAM = esm.getHString(); } 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(); if (esm.isNextSub("NAM9")) esm.skipHSub(); // Rest state. You shouldn't even be able to save during rest, but skip just in case. if (esm.isNextSub("RNAM")) /* int hoursLeft; float x, y, z; // resting position */ esm.skipHSub(); // 16 bytes mBounty = 0; esm.getHNOT(mBounty, "CNAM"); mBirthsign = esm.getHNOString("BNAM"); // Holds the names of the last used Alchemy apparatus. Don't need to import this ATM, // because our GUI auto-selects the best apparatus. if (esm.isNextSub("NAM0")) esm.skipHSub(); if (esm.isNextSub("NAM1")) esm.skipHSub(); if (esm.isNextSub("NAM2")) esm.skipHSub(); if (esm.isNextSub("NAM3")) esm.skipHSub(); mHasENAM = esm.getHNOT("ENAM", mENAM.mCellX, mENAM.mCellY); if (esm.isNextSub("LNAM")) esm.skipHSub(); while (esm.isNextSub("FNAM")) { FNAM fnam; esm.getHT( fnam.mRank, fnam.mUnknown1, fnam.mReputation, fnam.mFlags, fnam.mUnknown2, fnam.mFactionName.mData); mFactions.push_back(fnam); } mHasAADT = esm.getHNOT("AADT", mAADT.animGroupIndex, mAADT.mUnknown5); // Attack animation data? if (esm.isNextSub("KNAM")) esm.skipHSub(); // assigned Quick Keys, I think if (esm.isNextSub("ANIS")) esm.skipHSub(); // 16 bytes if (esm.isNextSub("WERE")) { // some werewolf data, 152 bytes // maybe current skills and attributes for werewolf form esm.getSubHeader(); esm.skip(152); } } } openmw-openmw-0.49.0/apps/essimporter/importplayer.hpp000066400000000000000000000064551503074453300232110ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_PLAYER_H #define OPENMW_ESSIMPORT_PLAYER_H #include #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// Other player data struct PCDT { int32_t mBounty; std::string mBirthsign; std::vector mKnownDialogueTopics; enum PlayerFlags { 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 }; 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 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 ENAM { int32_t mCellX; int32_t mCellY; }; 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 mHasENAM; ENAM mENAM; // last exterior cell bool mHasAADT; AADT mAADT; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importproj.cpp000066400000000000000000000010611503074453300226460ustar00rootroot00000000000000#include "importproj.h" #include namespace ESSImport { void ESSImport::PROJ::load(ESM::ESMReader& esm) { 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); } } } openmw-openmw-0.49.0/apps/essimporter/importproj.h000066400000000000000000000020151503074453300223130ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTPROJ_H #define OPENMW_ESSIMPORT_IMPORTPROJ_H #include #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct PROJ { 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); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importques.cpp000066400000000000000000000003511503074453300226520ustar00rootroot00000000000000#include "importques.hpp" #include namespace ESSImport { void QUES::load(ESM::ESMReader& esm) { while (esm.isNextSub("DATA")) mInfo.push_back(esm.getHString()); } } openmw-openmw-0.49.0/apps/essimporter/importques.hpp000066400000000000000000000011151503074453300226560ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTQUES_H #define OPENMW_ESSIMPORT_IMPORTQUES_H #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// State for a quest /// Presumably this record only exists when Tribunal is installed, /// since pre-Tribunal there weren't any quest names in the data files. struct QUES { std::string mName; // NAME, should be assigned from outside as usual std::vector mInfo; // list of journal entries for the quest void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importscpt.cpp000066400000000000000000000010031503074453300226410ustar00rootroot00000000000000#include "importscpt.hpp" #include namespace ESSImport { void SCPT::load(ESM::ESMReader& esm) { esm.getHNT("SCHD", mSCHD.mName.mData, mSCHD.mNumShorts, mSCHD.mNumLongs, mSCHD.mNumFloats, mSCHD.mScriptDataSize, mSCHD.mStringTableSize); mSCRI.load(esm); mRefNum = -1; if (esm.isNextSub("RNAM")) { mRunning = true; esm.getHT(mRefNum); } else mRunning = false; } } openmw-openmw-0.49.0/apps/essimporter/importscpt.hpp000066400000000000000000000014201503074453300226510ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTSCPT_H #define OPENMW_ESSIMPORT_IMPORTSCPT_H #include "importscri.hpp" #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct SCHD { 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 struct SCPT { SCHD mSCHD; // values of local variables SCRI mSCRI; bool mRunning; int32_t mRefNum; // Targeted reference, -1: no reference void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importscri.cpp000066400000000000000000000024761503074453300226470ustar00rootroot00000000000000#include "importscri.hpp" #include namespace ESSImport { void SCRI::load(ESM::ESMReader& esm) { mScript = esm.getHNOString("SCRI"); int32_t numShorts = 0, numLongs = 0, numFloats = 0; if (esm.isNextSub("SLCS")) { esm.getSubHeader(); esm.getT(numShorts); esm.getT(numLongs); esm.getT(numFloats); } if (esm.isNextSub("SLSD")) { esm.getSubHeader(); for (int i = 0; i < numShorts; ++i) { int16_t val; esm.getT(val); mShorts.push_back(val); } } // I haven't seen Longs in a save file yet, but SLLD would make sense for the name // TODO: test this if (esm.isNextSub("SLLD")) { esm.getSubHeader(); for (int i = 0; i < numLongs; ++i) { int32_t val; esm.getT(val); mLongs.push_back(val); } } if (esm.isNextSub("SLFD")) { esm.getSubHeader(); for (int i = 0; i < numFloats; ++i) { float val; esm.getT(val); mFloats.push_back(val); } } } } openmw-openmw-0.49.0/apps/essimporter/importscri.hpp000066400000000000000000000007521503074453300226470ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTSCRI_H #define OPENMW_ESSIMPORT_IMPORTSCRI_H #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { /// Local variable assignments for a running script struct SCRI { std::string mScript; std::vector mShorts; std::vector mLongs; std::vector mFloats; void load(ESM::ESMReader& esm); }; } #endif openmw-openmw-0.49.0/apps/essimporter/importsplm.cpp000066400000000000000000000026541503074453300226600ustar00rootroot00000000000000#include "importsplm.h" #include namespace ESSImport { void SPLM::load(ESM::ESMReader& esm) { while (esm.isNextSub("NAME")) { 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); } } } openmw-openmw-0.49.0/apps/essimporter/importsplm.h000066400000000000000000000032571503074453300223250ustar00rootroot00000000000000#ifndef OPENMW_ESSIMPORT_IMPORTSPLM_H #define OPENMW_ESSIMPORT_IMPORTSPLM_H #include #include #include namespace ESM { class ESMReader; } namespace ESSImport { struct SPLM { 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); }; } #endif openmw-openmw-0.49.0/apps/essimporter/main.cpp000066400000000000000000000051421503074453300213710ustar00rootroot00000000000000#include #include #include #include #include "importer.hpp" namespace bpo = boost::program_options; int main(int argc, char** argv) { try { bpo::options_description desc(R"(Syntax: openmw-essimporter infile.ess outfile.omwsave Allowed options)"); bpo::positional_options_description p_desc; 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::store(parsed, variables); if (variables.count("help") || !variables.count("mwsave") || !variables.count("output")) { std::cout << desc; return 0; } bpo::notify(variables); Files::ConfigurationManager cfgManager(true); cfgManager.processPaths(variables, std::filesystem::current_path()); cfgManager.readConfiguration(variables, desc); const auto& essFile = variables["mwsave"].as(); const auto& outputFile = variables["output"].as(); std::string encoding = variables["encoding"].as(); ESSImport::Importer importer(essFile, outputFile, encoding); if (variables.count("compare")) importer.compare(); else { 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?"); } importer.run(); } } catch (std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } return 0; } openmw-openmw-0.49.0/apps/launcher/000077500000000000000000000000001503074453300171645ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/launcher/CMakeLists.txt000066400000000000000000000041761503074453300217340ustar00rootroot00000000000000set(LAUNCHER datafilespage.cpp graphicspage.cpp sdlinit.cpp main.cpp maindialog.cpp textslotmsgbox.cpp importpage.cpp settingspage.cpp utils/cellnameloader.cpp utils/profilescombobox.cpp utils/textinputdialog.cpp utils/lineedit.cpp utils/openalutil.cpp ${CMAKE_SOURCE_DIR}/files/windows/launcher.rc ) set(LAUNCHER_HEADER datafilespage.hpp graphicspage.hpp sdlinit.hpp maindialog.hpp textslotmsgbox.hpp importpage.hpp settingspage.hpp utils/cellnameloader.hpp utils/profilescombobox.hpp utils/textinputdialog.hpp utils/lineedit.hpp utils/openalutil.hpp ) 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) QT_ADD_RESOURCES(RCC_SRCS ${LAUNCHER_RES}) if(NOT WIN32) include_directories(${LIBUNSHIELD_INCLUDE_DIR}) endif(NOT WIN32) # Main executable openmw_add_executable(openmw-launcher ${GUI_TYPE} ${LAUNCHER} ${LAUNCHER_HEADER} ${RCC_SRCS} ) add_dependencies(openmw-launcher qm-files) if (WIN32) INSTALL(TARGETS openmw-launcher RUNTIME DESTINATION ".") endif (WIN32) target_link_libraries(openmw-launcher SDL2::SDL2 ${OPENAL_LIBRARY} components_qt ) target_link_libraries(openmw-launcher Qt::Widgets Qt::Core Qt::Svg) if (BUILD_WITH_CODE_COVERAGE) 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) set_property(TARGET openmw-launcher PROPERTY AUTOUIC ON) set_property(TARGET openmw-launcher PROPERTY AUTOUIC_SEARCH_PATHS ui) endif(USE_QT) if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(openmw-launcher PRIVATE ) endif() openmw-openmw-0.49.0/apps/launcher/datafilespage.cpp000066400000000000000000001211241503074453300224620ustar00rootroot00000000000000#include "datafilespage.hpp" #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 #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; }); return sortedItems; } } 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)) , mReloadCellsThread(&DataFilesPage::reloadCells, this) { 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(), &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, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this, &DataFilesPage::slotAddonDataChanged); mReloadCellsTimer = new QTimer(this); mReloadCellsTimer->setSingleShot(true); mReloadCellsTimer->setInterval(200); connect(mReloadCellsTimer, &QTimer::timeout, this, &DataFilesPage::onReloadCellsTimerTimeout); // Call manually to indicate all changes to addon data during startup. onReloadCellsTimerTimeout(); } Launcher::DataFilesPage::~DataFilesPage() { { const std::lock_guard lock(mReloadCellsMutex); mAbortReloadCells = true; mStartReloadCells.notify_one(); } mReloadCellsThread.join(); } void Launcher::DataFilesPage::buildView() { 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"); // combo box ui.profilesComboBox->addItem(mDefaultContentListName); 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); refreshButton->setDefaultAction(ui.refreshDataFilesAction); // 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.refreshDataFilesAction, &QAction::triggered, this, &DataFilesPage::slotRefreshButtonClicked); connect(ui.updateNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::startNavMeshTool); connect(ui.cancelNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::killNavMeshTool); 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(); buildDataFilesContextMenu(); } void Launcher::DataFilesPage::slotCopySelectedItemsPaths() { QClipboard* clipboard = QApplication::clipboard(); QStringList filepaths; for (QListWidgetItem* item : ui.directoryListWidget->selectedItems()) { QString path = qvariant_cast(item->data(Qt::UserRole)).originalRepresentation; filepaths.push_back(path); } if (!filepaths.isEmpty()) { clipboard->setText(filepaths.join("\n")); } } void Launcher::DataFilesPage::slotOpenSelectedItemsPaths() { QListWidgetItem* item = ui.directoryListWidget->currentItem(); QUrl confFolderUrl = QUrl::fromLocalFile(qvariant_cast(item->data(Qt::UserRole)).value); QDesktopServices::openUrl(confFolderUrl); } 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())); } void Launcher::DataFilesPage::buildDataFilesContextMenu() { connect(ui.directoryListWidget, &QListWidget::customContextMenuRequested, this, &DataFilesPage::slotShowDataFilesContextMenu); mDataFilesContextMenu = new QMenu(ui.directoryListWidget); mDataFilesContextMenu->addAction( tr("&Copy Path(s) to Clipboard"), this, &Launcher::DataFilesPage::slotCopySelectedItemsPaths); mDataFilesContextMenu->addAction( tr("&Open Path in File Explorer"), this, &Launcher::DataFilesPage::slotOpenSelectedItemsPaths); } 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); // 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) { mSelector->clearFiles(); ui.archiveListWidget->clear(); ui.directoryListWidget->clear(); 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(mGameSettings.processPathSettingValue({ dir })); } mDataLocal = mGameSettings.getDataLocal(); if (!mDataLocal.isEmpty()) directories.insert(0, { mDataLocal }); const auto& resourcesVfs = mGameSettings.getResourcesVfs(); if (!resourcesVfs.isEmpty()) directories.insert(0, { resourcesVfs }); QIcon containsDataIcon(":/images/openmw-plugin.png"); QProgressDialog progressBar("Adding data directories", {}, 0, static_cast(directories.size()), this); progressBar.setWindowModality(Qt::WindowModal); std::unordered_set visitedDirectories; for (qsizetype i = 0; i < directories.size(); ++i) { progressBar.setValue(static_cast(i)); const Config::SettingValue& currentDir = directories.at(i); if (!visitedDirectories.insert(currentDir.value).second) continue; // add new achives files presents in current directory addArchivesFromDir(currentDir.value); QStringList tooltip; // 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); // 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(containsDataIcon); 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')); } progressBar.setValue(progressBar.maximum()); mSelector->sortFiles(); QList selectedArchives = mGameSettings.getArchiveList(); QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName); if (!contentModelSelectedArchives.isEmpty()) { 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 }); } // sort and tick BSA according to profile int row = 0; for (const auto& archive : selectedArchives) { const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchFixedString); if (match.isEmpty()) continue; const auto name = match[0]->text(); const auto oldrow = ui.archiveListWidget->row(match[0]); // entries may be duplicated, e.g. if a content list predated a BSA being added to a non-user config file if (oldrow < row) continue; 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++; } 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) { Settings::navigator().mMaxNavmeshdbFileSize.set( static_cast(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024); QString profileName = profile; if (profileName.isEmpty()) profileName = ui.profilesComboBox->currentText(); // 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) { fileNames.append(item->fileName()); } 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() const { // retrieve the files selected for the profile ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); QStringList filePaths; for (const ContentSelectorModel::EsmFile* item : items) if (QFile::exists(item->filePath())) filePaths.append(item->filePath()); return filePaths; } void Launcher::DataFilesPage::removeProfile(const QString& profile) { mLauncherSettings.removeContentList(profile); } QAbstractItemModel* Launcher::DataFilesPage::profilesModel() const { return ui.profilesComboBox->model(); } int Launcher::DataFilesPage::profilesIndex() const { return ui.profilesComboBox->currentIndex(); } void Launcher::DataFilesPage::setProfile(int index, bool savePrevious) { if (index >= -1 && index < ui.profilesComboBox->count()) { QString previous = mPreviousProfile; QString current = ui.profilesComboBox->itemText(index); mPreviousProfile = current; setProfile(previous, current, savePrevious); } } void Launcher::DataFilesPage::setProfile(const QString& previous, const QString& current, bool savePrevious) { // abort if no change (poss. duplicate signal) if (previous == current) return; if (!previous.isEmpty() && savePrevious) saveSettings(previous); 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) { removeProfile(item); } void Launcher::DataFilesPage::refreshDataFilesView() { QString currentProfile = ui.profilesComboBox->currentText(); saveSettings(currentProfile); populateFileViews(currentProfile); } void Launcher::DataFilesPage::slotRefreshButtonClicked() { refreshDataFilesView(); } void Launcher::DataFilesPage::slotProfileChangedByUser(const QString& previous, const QString& current) { setProfile(previous, current, true); emit signalProfileChanged(ui.profilesComboBox->findText(current)); } void Launcher::DataFilesPage::slotProfileRenamed(const QString& previous, const QString& current) { if (previous.isEmpty()) return; // Save the new profile name saveSettings(); // Remove the old one removeProfile(previous); loadSettings(); } void Launcher::DataFilesPage::slotProfileChanged(int index) { // in case the event was triggered externally if (ui.profilesComboBox->currentIndex() != index) ui.profilesComboBox->setCurrentIndex(index); setProfile(index, true); } void Launcher::DataFilesPage::on_newProfileAction_triggered() { if (mNewProfileDialog->exec() != QDialog::Accepted) return; QString profile = mNewProfileDialog->lineEdit()->text(); if (profile.isEmpty()) return; saveSettings(); mLauncherSettings.setCurrentContentListName(profile); addProfile(profile, true); } void Launcher::DataFilesPage::addProfile(const QString& profile, bool setAsCurrent) { if (profile.isEmpty()) return; if (ui.profilesComboBox->findText(profile) == -1) ui.profilesComboBox->addItem(profile); if (setAsCurrent) setProfile(ui.profilesComboBox->findText(profile), false); } void Launcher::DataFilesPage::on_cloneProfileAction_triggered() { if (mCloneProfileDialog->exec() != QDialog::Accepted) return; QString profile = mCloneProfileDialog->lineEdit()->text(); if (profile.isEmpty()) return; 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); } void Launcher::DataFilesPage::on_deleteProfileAction_triggered() { QString profile = ui.profilesComboBox->currentText(); if (profile.isEmpty()) return; 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; // changing the profile forces a reload of plugin file views. ui.profilesComboBox->setCurrentIndex(next); removeProfile(profile); ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile)); checkForDefaultProfile(); } 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) { // 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"), {}, 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::slotShowDataFilesContextMenu(const QPoint& pos) { QPoint globalPos = ui.directoryListWidget->viewport()->mapToGlobal(pos); mDataFilesContextMenu->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(VFS::Path::normalizedFromQString(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(VFS::Path::normalizedFromQString(fileName)).second) addArchive(fileName, Qt::Unchecked); } } void Launcher::DataFilesPage::checkForDefaultProfile() { // don't allow deleting "Default" profile bool success = (ui.profilesComboBox->currentText() != mDefaultContentListName); ui.deleteProfileAction->setEnabled(success); ui.profilesComboBox->setEditEnabled(success); } bool Launcher::DataFilesPage::showDeleteMessageBox(const QString& text) { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Delete Content List")); msgBox.setIcon(QMessageBox::Warning); 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); msgBox.exec(); return (msgBox.clickedButton() == deleteButton); } void Launcher::DataFilesPage::slotAddonDataChanged() { mReloadCellsTimer->start(); } void Launcher::DataFilesPage::onReloadCellsTimerTimeout() { const ContentSelectorModel::ContentFileList items = mSelector->selectedFiles(); QStringList selectedFiles; for (const ContentSelectorModel::EsmFile* item : items) selectedFiles.append(item->filePath()); if (mSelectedFiles != selectedFiles) { const std::lock_guard lock(mReloadCellsMutex); mSelectedFiles = std::move(selectedFiles); mReloadCells = true; mStartReloadCells.notify_one(); } } void Launcher::DataFilesPage::reloadCells() { QStringList selectedFiles; std::unique_lock lock(mReloadCellsMutex); while (true) { mStartReloadCells.wait(lock); if (mAbortReloadCells) return; if (!std::exchange(mReloadCells, false)) continue; const QStringList newSelectedFiles = mSelectedFiles; lock.unlock(); QStringList filteredFiles; for (const QString& v : newSelectedFiles) if (QFile::exists(v)) filteredFiles.append(v); if (selectedFiles != filteredFiles) { selectedFiles = std::move(filteredFiles); CellNameLoader cellNameLoader; QSet set = cellNameLoader.getCellNames(selectedFiles); QStringList cellNamesList(set.begin(), set.end()); std::sort(cellNamesList.begin(), cellNamesList.end()); emit signalLoadedCellsChanged(std::move(cellNamesList)); } lock.lock(); if (mAbortReloadCells) return; } } 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); } openmw-openmw-0.49.0/apps/launcher/datafilespage.hpp000066400000000000000000000123141503074453300224670ustar00rootroot00000000000000#ifndef DATAFILESPAGE_H #define DATAFILESPAGE_H #include "ui_datafilespage.h" #include #include #include #include #include #include #include #include class QSortFilterProxyModel; class QAbstractItemModel; class QMenu; class QTimer; namespace Files { struct ConfigurationManager; } namespace ContentSelectorView { class ContentSelector; } namespace Config { class GameSettings; struct SettingValue; class LauncherSettings; } namespace Launcher { class MainDialog; class TextInputDialog; class ProfilesComboBox; class DataFilesPage : public QWidget { Q_OBJECT ContentSelectorView::ContentSelector* mSelector; Ui::DataFilesPage ui; QMenu* mArchiveContextMenu; QMenu* mDataFilesContextMenu; public: explicit DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings, Config::LauncherSettings& launcherSettings, MainDialog* parent = nullptr); ~DataFilesPage(); QAbstractItemModel* profilesModel() const; int profilesIndex() const; // void writeConfig(QString profile = QString()); void saveSettings(const QString& profile = ""); bool loadSettings(); signals: void signalProfileChanged(int index); void signalLoadedCellsChanged(QStringList selectedFiles); public slots: void slotProfileChanged(int index); private slots: 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 addSubdirectories(bool append); void sortDirectories(); void sortArchives(); void removeDirectory(); void moveSources(QListWidget* sourceList, int step); void slotShowArchiveContextMenu(const QPoint& pos); void slotShowDataFilesContextMenu(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; private: struct NavMeshToolProgress { bool mEnabled = true; QByteArray mLogData; QByteArray mMessagesData; std::map mWorldspaces; int mCellsCount = 0; int mExpectedMaxProgress = 0; }; MainDialog* mMainDialog; TextInputDialog* mNewProfileDialog; TextInputDialog* mCloneProfileDialog; const Files::ConfigurationManager& mCfgMgr; Config::GameSettings& mGameSettings; Config::LauncherSettings& mLauncherSettings; QString mPreviousProfile; QStringList mSelectedFiles; QString mDataLocal; QStringList mKnownArchives; QStringList mNewDataDirs; Process::ProcessInvoker* mNavMeshToolInvoker; NavMeshToolProgress mNavMeshToolProgress; bool mReloadCells = false; bool mAbortReloadCells = false; std::mutex mReloadCellsMutex; std::condition_variable mStartReloadCells; std::thread mReloadCellsThread; QTimer* mReloadCellsTimer; void addArchive(const QString& name, Qt::CheckState selected, int row = -1); void addArchivesFromDir(const QString& dir); void buildView(); void buildArchiveContextMenu(); void buildDataFilesContextMenu(); 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 onReloadCellsTimerTimeout(); void reloadCells(); void refreshDataFilesView(); void updateNavMeshProgress(int minDataSize); void slotCopySelectedItemsPaths(); void slotOpenSelectedItemsPaths(); /** * 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 openmw-openmw-0.49.0/apps/launcher/graphicspage.cpp000066400000000000000000000205571503074453300223360ustar00rootroot00000000000000#include "graphicspage.hpp" #include "sdlinit.hpp" #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 #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED #include #include Launcher::GraphicsPage::GraphicsPage(QWidget* parent) : QWidget(parent) { setObjectName("GraphicsPage"); setupUi(this); // Set the maximum res we can set in windowed mode QRect res = getMaximumResolution(); customWidthSpinBox->setMaximum(res.width()); customHeightSpinBox->setMaximum(res.height()); 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() { bool sdlConnectSuccessful = initSDL(); if (!sdlConnectSuccessful) { return false; } int displays = SDL_GetNumVideoDisplays(); if (displays < 0) { QMessageBox msgBox; 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.exec(); return false; } screenComboBox->clear(); mResolutionsPerScreen.clear(); for (int i = 0; i < displays; i++) { mResolutionsPerScreen.append(getAvailableResolutions(i)); screenComboBox->addItem(QString(tr("Screen ")) + QString::number(i + 1)); } screenChanged(0); // Disconnect from SDL processes quitSDL(); return true; } bool Launcher::GraphicsPage::loadSettings() { if (!setupSDL()) return false; // Visuals 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::video().mWindowBorder) windowBorderCheckBox->setCheckState(Qt::Checked); // aaValue is the actual value (0, 1, 2, 4, 8, 16) const int aaValue = Settings::video().mAntialiasing; // aaIndex is the index into the allowed values in the pull down. const int aaIndex = antiAliasingComboBox->findText(QString::number(aaValue)); if (aaIndex != -1) antiAliasingComboBox->setCurrentIndex(aaIndex); 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) { standardRadioButton->toggle(); resolutionComboBox->setCurrentIndex(resIndex); } else { customRadioButton->toggle(); customWidthSpinBox->setValue(width); customHeightSpinBox->setValue(height); } const float fpsLimit = Settings::video().mFramerateLimit; if (fpsLimit != 0) { framerateLimitCheckBox->setCheckState(Qt::Checked); framerateLimitSpinBox->setValue(fpsLimit); } return true; } void Launcher::GraphicsPage::saveSettings() { // Visuals 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()) { 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 { cWidth = customWidthSpinBox->value(); cHeight = customHeightSpinBox->value(); } Settings::video().mResolutionX.set(cWidth); Settings::video().mResolutionY.set(cHeight); Settings::video().mScreen.set(screenComboBox->currentIndex()); if (framerateLimitCheckBox->checkState() != Qt::Unchecked) { Settings::video().mFramerateLimit.set(framerateLimitSpinBox->value()); } else if (Settings::video().mFramerateLimit != 0) { Settings::video().mFramerateLimit.set(0); } } QStringList Launcher::GraphicsPage::getAvailableResolutions(int screen) { QStringList result; SDL_DisplayMode mode; int modeIndex, modes = SDL_GetNumDisplayModes(screen); if (modes < 0) { QMessageBox msgBox; 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.exec(); return result; } for (modeIndex = 0; modeIndex < modes; modeIndex++) { if (SDL_GetDisplayMode(screen, modeIndex, &mode) < 0) { QMessageBox msgBox; 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.exec(); return result; } auto str = Misc::getResolutionText(mode.w, mode.h, "%i × %i (%i:%i)"); result.append(QString(str.c_str())); } result.removeDuplicates(); return result; } QRect Launcher::GraphicsPage::getMaximumResolution() { QRect max; for (QScreen* screen : QGuiApplication::screens()) { QRect res = screen->geometry(); if (res.width() > max.width()) max.setWidth(res.width()); if (res.height() > max.height()) max.setHeight(res.height()); } return max; } void Launcher::GraphicsPage::screenChanged(int screen) { if (screen >= 0) { resolutionComboBox->clear(); resolutionComboBox->addItems(mResolutionsPerScreen[screen]); } } void Launcher::GraphicsPage::slotFullScreenChanged(int mode) { 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 { customRadioButton->setEnabled(true); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); windowBorderCheckBox->setEnabled(true); } } void Launcher::GraphicsPage::slotStandardToggled(bool checked) { if (checked) { resolutionComboBox->setEnabled(true); customWidthSpinBox->setEnabled(false); customHeightSpinBox->setEnabled(false); } else { resolutionComboBox->setEnabled(false); customWidthSpinBox->setEnabled(true); customHeightSpinBox->setEnabled(true); } } void Launcher::GraphicsPage::slotFramerateLimitToggled(bool checked) { framerateLimitSpinBox->setEnabled(checked); } openmw-openmw-0.49.0/apps/launcher/graphicspage.hpp000066400000000000000000000016761503074453300223440ustar00rootroot00000000000000#ifndef GRAPHICSPAGE_H #define GRAPHICSPAGE_H #include "ui_graphicspage.h" #include namespace Files { struct ConfigurationManager; } namespace Launcher { class GraphicsSettings; class GraphicsPage : public QWidget, private Ui::GraphicsPage { Q_OBJECT public: explicit GraphicsPage(QWidget* parent = nullptr); void saveSettings(); bool loadSettings(); public slots: void screenChanged(int screen); private slots: void slotFullScreenChanged(int state); void slotStandardToggled(bool checked); void slotFramerateLimitToggled(bool checked); private: QVector mResolutionsPerScreen; static QStringList getAvailableResolutions(int screen); static QRect getMaximumResolution(); bool setupSDL(); void handleWindowModeChange(Settings::WindowMode state); }; } #endif openmw-openmw-0.49.0/apps/launcher/importpage.cpp000066400000000000000000000150131503074453300220370ustar00rootroot00000000000000#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() { mLauncherSettings.setImportContentSetup(addonsCheckBox->isChecked()); mLauncherSettings.setImportFontSetup(fontsCheckBox->isChecked()); } bool Launcher::ImportPage::loadSettings() { addonsCheckBox->setChecked(mLauncherSettings.getImportContentSetup()); fontsCheckBox->setChecked(mLauncherSettings.getImportFontSetup()); return true; } openmw-openmw-0.49.0/apps/launcher/importpage.hpp000066400000000000000000000027061503074453300220510ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/launcher/main.cpp000066400000000000000000000041501503074453300206140ustar00rootroot00000000000000#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 #define MAC_OS_X_VERSION_MIN_REQUIRED __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ #endif // MAC_OS_X_VERSION_MIN_REQUIRED #include "maindialog.hpp" int runLauncher(int argc, char* 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); Debug::setupLogging(configurationManager.getLogPath(), "Launcher"); try { Platform::Application app(argc, argv); QString resourcesPath("."); if (!variables["resources"].empty()) { resourcesPath = Files::pathToQString(variables["resources"].as().u8string()); } l10n::installQtTranslations(app, "launcher", resourcesPath); Launcher::MainDialog mainWin(configurationManager); Launcher::FirstRunDialogResult result = mainWin.showFirstRunDialog(); if (result == Launcher::FirstRunDialogResultFailure) return 0; if (result == Launcher::FirstRunDialogResultContinue) mainWin.show(); int exitCode = app.exec(); return exitCode; } catch (const std::exception& e) { Log(Debug::Error) << "Unexpected exception: " << e.what(); return 0; } } int main(int argc, char* argv[]) { return Debug::wrapApplication(runLauncher, argc, argv, "Launcher"); } openmw-openmw-0.49.0/apps/launcher/maindialog.cpp000066400000000000000000000452141503074453300220020ustar00rootroot00000000000000#include "maindialog.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "datafilespage.hpp" #include "graphicspage.hpp" #include "importpage.hpp" #include "settingspage.hpp" namespace { constexpr const char* toolBarStyle = "QToolBar { border: 0px; } QToolButton { min-width: 70px }"; } using namespace Process; void cfgError(const QString& title, const QString& msg) { QMessageBox msgBox; msgBox.setWindowTitle(title); msgBox.setIcon(QMessageBox::Critical); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setText(msg); msgBox.exec(); } 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(), &QProcess::started, this, &MainDialog::wizardStarted); connect(mWizardInvoker->getProcess(), qOverload(&QProcess::finished), this, &MainDialog::wizardFinished); buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Launch OpenMW")); buttonBox->button(QDialogButtonBox::Help)->setText(tr("Help")); buttonBox->button(QDialogButtonBox::Ok)->setMinimumWidth(160); // 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, &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() { delete mGameInvoker; 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("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() { // Avoid creating the widgets twice if (pagesWidget->count() != 0) return; mDataFilesPage = new DataFilesPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mGraphicsPage = new GraphicsPage(this); mImportPage = new ImportPage(mCfgMgr, mGameSettings, mLauncherSettings, this); mSettingsPage = new SettingsPage(mGameSettings, this); // Add the pages to the stacked widget pagesWidget->addWidget(mDataFilesPage); pagesWidget->addWidget(mGraphicsPage); pagesWidget->addWidget(mSettingsPage); pagesWidget->addWidget(mImportPage); // Select the first page dataAction->setChecked(true); // Using Qt::QueuedConnection because signal is emitted in a subthread and slot is in the main thread connect(mDataFilesPage, &DataFilesPage::signalLoadedCellsChanged, mSettingsPage, &SettingsPage::slotLoadedCellsChanged, Qt::QueuedConnection); } Launcher::FirstRunDialogResult Launcher::MainDialog::showFirstRunDialog() { if (!setupLauncherSettings()) return FirstRunDialogResultFailure; // 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.

")); QAbstractButton* wizardButton = msgBox.addButton(tr("Run &Installation Wizard"), QMessageBox::AcceptRole); // ActionRole doesn't work?! QAbstractButton* skipButton = msgBox.addButton(tr("Skip"), QMessageBox::RejectRole); msgBox.exec(); if (msgBox.clickedButton() == wizardButton) { if (mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return FirstRunDialogResultWizard; } else if (msgBox.clickedButton() == skipButton) { // Don't bother setting up absent game data. if (setup()) return FirstRunDialogResultContinue; } return FirstRunDialogResultFailure; } if (!setup() || !setupGameData()) { return FirstRunDialogResultFailure; } return FirstRunDialogResultContinue; } void Launcher::MainDialog::setVersionLabel() { // Add version information to bottom of the window 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 (!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))); } bool Launcher::MainDialog::setup() { if (!setupGameSettings()) return false; setVersionLabel(); mLauncherSettings.setContentList(mGameSettings); if (!setupGraphicsSettings()) return false; // Now create the pages as they need the settings createPages(); // Call this so we can exit on SDL errors before mainwindow is shown if (!mGraphicsPage->loadSettings()) return false; loadSettings(); return true; } bool Launcher::MainDialog::reloadSettings() { if (!setupLauncherSettings()) return false; if (!setupGameSettings()) return false; mLauncherSettings.setContentList(mGameSettings); if (!setupGraphicsSettings()) return false; if (!mImportPage->loadSettings()) return false; if (!mDataFilesPage->loadSettings()) return false; if (!mGraphicsPage->loadSettings()) return false; if (!mSettingsPage->loadSettings()) return false; return true; } void Launcher::MainDialog::enableDataPage() { pagesWidget->setCurrentIndex(0); mImportPage->resetProgressBar(); dataAction->setChecked(true); graphicsAction->setChecked(false); importAction->setChecked(false); settingsAction->setChecked(false); } 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(); const QString path = Files::pathToQString(mCfgMgr.getUserConfigPath() / Config::LauncherSettings::sLauncherConfigFileName); if (!QFile::exists(path)) return true; Log(Debug::Info) << "Loading config file: " << path.toUtf8().constData(); QFile file(path); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 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; } bool Launcher::MainDialog::setupGameSettings() { mGameSettings.clear(); QFile file; 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 {}; } QTextStream stream(&file); Misc::ensureUtf8Encoding(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; } bool Launcher::MainDialog::setupGameData() { bool foundData = false; // Check if the paths actually contain data files for (const auto& path3 : mGameSettings.getDataDirs()) { QDir dir(path3.value); QStringList filters; filters << "*.esp" << "*.esm" << "*.omwgame" << "*.omwaddon"; if (!dir.entryList(filters).isEmpty()) { foundData = true; break; } } 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.")); 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 msgBox.exec(); if (msgBox.clickedButton() == wizardButton) { if (!mWizardInvoker->startProcess(QLatin1String("openmw-wizard"), false)) return false; } } return true; } bool Launcher::MainDialog::setupGraphicsSettings() { 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) { 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; } } void Launcher::MainDialog::loadSettings() { const auto& mainWindow = mLauncherSettings.getMainWindow(); resize(mainWindow.mWidth, mainWindow.mHeight); move(mainWindow.mPosX, mainWindow.mPosY); } void Launcher::MainDialog::saveSettings() { mLauncherSettings.setMainWindow(Config::LauncherSettings::MainWindow{ .mWidth = width(), .mHeight = height(), .mPosX = pos().x(), .mPosY = pos().y(), }); mLauncherSettings.resetFirstRun(); } bool Launcher::MainDialog::writeSettings() { // Now write all config files saveSettings(); mDataFilesPage->saveSettings(); mGraphicsPage->saveSettings(); mImportPage->saveSettings(); mSettingsPage->saveSettings(); const auto& userPath = mCfgMgr.getUserConfigPath(); 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 #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)) { // 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())); return false; } mGameSettings.writeFileWithComments(file); file.close(); // Graphics settings const auto settingsPath = mCfgMgr.getUserConfigPath() / "settings.cfg"; try { Settings::Manager::saveUser(settingsPath); } 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(Files::pathToQString(userPath / Config::LauncherSettings::sLauncherConfigFileName)); 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())); return false; } QTextStream stream(&file); stream.setDevice(&file); Misc::ensureUtf8Encoding(stream); mLauncherSettings.writeFile(stream); file.close(); return true; } void Launcher::MainDialog::closeEvent(QCloseEvent* event) { writeSettings(); event->accept(); } void Launcher::MainDialog::wizardStarted() { hide(); } void Launcher::MainDialog::wizardFinished(int exitCode, QProcess::ExitStatus exitStatus) { if (exitCode != 0 || exitStatus == QProcess::CrashExit) return qApp->quit(); // HACK: Ensure the pages are created, else segfault setup(); if (setupGameData() && reloadSettings()) show(); } void Launcher::MainDialog::play() { if (!writeSettings()) return qApp->quit(); 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(); return; } // Launch the game detached if (mGameInvoker->startProcess(QLatin1String("openmw"), true)) return qApp->quit(); } void Launcher::MainDialog::help() { Misc::HelpViewer::openHelp("reference/index.html"); } openmw-openmw-0.49.0/apps/launcher/maindialog.hpp000066400000000000000000000047641503074453300220140ustar00rootroot00000000000000#ifndef MAINDIALOG_H #define MAINDIALOG_H #ifndef Q_MOC_RUN #include #include #include #endif #include "ui_mainwindow.h" class QListWidgetItem; class QStackedWidget; class QStringListModel; class QString; namespace Files { struct ConfigurationManager; } namespace Launcher { class GraphicsPage; class DataFilesPage; class UnshieldThread; class ImportPage; class SettingsPage; enum FirstRunDialogResult { FirstRunDialogResultFailure, FirstRunDialogResultContinue, FirstRunDialogResultWizard }; #ifndef WIN32 bool expansions(Launcher::UnshieldThread& cd); #endif class MainDialog : public QMainWindow, private Ui::MainWindow { Q_OBJECT public: explicit MainDialog(const Files::ConfigurationManager& configurationManager, QWidget* parent = nullptr); ~MainDialog() override; FirstRunDialogResult showFirstRunDialog(); bool reloadSettings(); bool writeSettings(); public slots: 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); private: bool setup(); void createIcons(); void createPages(); bool setupLauncherSettings(); bool setupGameSettings(); bool setupGraphicsSettings(); bool setupGameData(); void setVersionLabel(); 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); void closeEvent(QCloseEvent* event) override; GraphicsPage* mGraphicsPage; DataFilesPage* mDataFilesPage; ImportPage* mImportPage; SettingsPage* mSettingsPage; Process::ProcessInvoker* mGameInvoker; Process::ProcessInvoker* mWizardInvoker; const Files::ConfigurationManager& mCfgMgr; Config::GameSettings mGameSettings; Config::LauncherSettings mLauncherSettings; }; } #endif openmw-openmw-0.49.0/apps/launcher/sdlinit.cpp000066400000000000000000000010341503074453300213340ustar00rootroot00000000000000#include #include bool initSDL() { SDL_SetHint(SDL_HINT_RENDER_DRIVER, "software"); SDL_SetMainReady(); // Required for determining screen resolution and such on the Graphics tab if (SDL_Init(SDL_INIT_VIDEO) != 0) { return false; } signal(SIGINT, SIG_DFL); // We don't want to use the SDL event loop in the launcher, // so reset SIGINT which SDL wants to redirect to an SDL_Quit event. return true; } void quitSDL() { // Disconnect from SDL processes SDL_Quit(); } openmw-openmw-0.49.0/apps/launcher/sdlinit.hpp000066400000000000000000000001161503074453300213410ustar00rootroot00000000000000#ifndef SDLINIT_H #define SDLINIT_H bool initSDL(); void quitSDL(); #endif openmw-openmw-0.49.0/apps/launcher/settingspage.cpp000066400000000000000000000663301503074453300223750ustar00rootroot00000000000000#include "settingspage.hpp" #include #include #include #include #include #include #include #include #include "utils/openalutil.hpp" namespace { void loadSettingBool(const Settings::SettingValue& value, QCheckBox& checkbox) { checkbox.setCheckState(value ? Qt::Checked : Qt::Unchecked); } void saveSettingBool(const QCheckBox& checkbox, Settings::SettingValue& value) { value.set(checkbox.checkState() == Qt::Checked); } void loadSettingInt(const Settings::SettingValue& value, QComboBox& comboBox) { comboBox.setCurrentIndex(value); } void loadSettingInt(const Settings::SettingValue& value, QComboBox& comboBox) { comboBox.setCurrentIndex(static_cast(value.get())); } void saveSettingInt(const QComboBox& comboBox, Settings::SettingValue& value) { value.set(comboBox.currentIndex()); } void saveSettingInt(const QComboBox& comboBox, Settings::SettingValue& value) { value.set(static_cast(comboBox.currentIndex())); } void loadSettingInt(const Settings::SettingValue& value, QSpinBox& spinBox) { spinBox.setValue(value); } void saveSettingInt(const QSpinBox& spinBox, Settings::SettingValue& value) { value.set(spinBox.value()); } int toIndex(Settings::HrtfMode value) { switch (value) { case Settings::HrtfMode::Auto: return 0; case Settings::HrtfMode::Disable: return 1; case Settings::HrtfMode::Enable: return 2; } return 0; } } 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); } void Launcher::SettingsPage::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::SettingsPage::on_skipMenuCheckBox_stateChanged(int state) { startDefaultCharacterAtLabel->setEnabled(state == Qt::Checked); startDefaultCharacterAtField->setEnabled(state == Qt::Checked); } void Launcher::SettingsPage::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 unitRadius / CellSizeInUnits; } int convertToUnits(double CellGridRadius) { return static_cast(CellSizeInUnits * CellGridRadius); } } bool Launcher::SettingsPage::loadSettings() { // 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); const int shadowDistLimit = Settings::shadows().mMaximumShadowMapDistance; if (shadowDistLimit > 0) { shadowDistanceCheckBox->setCheckState(Qt::Checked); shadowDistanceSpinBox->setValue(shadowDistLimit); shadowDistanceSpinBox->setEnabled(true); fadeStartSpinBox->setEnabled(true); } const float shadowFadeStart = Settings::shadows().mShadowFadeStart; if (shadowFadeStart != 0) fadeStartSpinBox->setValue(shadowFadeStart); 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); } connect(shadowDistanceCheckBox, &QCheckBox::toggled, this, &SettingsPage::slotShadowDistLimitToggled); 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); } // 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); } // 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); } // Bug fixes { loadSettingBool(Settings::game().mPreventMerchantEquipping, *preventMerchantEquippingCheckBox); loadSettingBool( Settings::game().mTrainersTrainingSkillsBasedOnBaseSkill, *trainersTrainingSkillsBasedOnBaseSkillCheckBox); } // Miscellaneous { // Saves loadSettingInt(Settings::saves().mMaxQuicksaves, *maximumQuicksavesComboBox); // Other Settings QString screenshotFormatString = QString::fromStdString(Settings::general().mScreenshotFormat).toUpper(); if (screenshotFormatComboBox->findText(screenshotFormatString) == -1) screenshotFormatComboBox->addItem(screenshotFormatString); screenshotFormatComboBox->setCurrentIndex(screenshotFormatComboBox->findText(screenshotFormatString)); 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::saveSettings() { // Game mechanics { 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); } // Visuals { 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); } 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"); } // Audio { if (audioDeviceSelectorComboBox->currentIndex() != 0) Settings::sound().mDevice.set(audioDeviceSelectorComboBox->currentText().toStdString()); else Settings::sound().mDevice.set({}); static constexpr std::array hrtfModes{ Settings::HrtfMode::Auto, Settings::HrtfMode::Disable, Settings::HrtfMode::Enable, }; Settings::sound().mHrtfEnable.set(hrtfModes[enableHRTFComboBox->currentIndex()]); 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()); } // 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); 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::slotLoadedCellsChanged(QStringList cellNames) { loadCellsForAutocomplete(std::move(cellNames)); } void Launcher::SettingsPage::slotAnimSourcesToggled(bool checked) { weaponSheathingCheckBox->setEnabled(checked); shieldSheathingCheckBox->setEnabled(checked); if (!checked) { weaponSheathingCheckBox->setCheckState(Qt::Unchecked); shieldSheathingCheckBox->setCheckState(Qt::Unchecked); } } void Launcher::SettingsPage::slotPostProcessToggled(bool checked) { postprocessTransparentPostpassCheckBox->setEnabled(checked); postprocessHDRTimeComboBox->setEnabled(checked); postprocessHDRTimeLabel->setEnabled(checked); } void Launcher::SettingsPage::slotSkyBlendingToggled(bool checked) { skyBlendingStartComboBox->setEnabled(checked); skyBlendingStartLabel->setEnabled(checked); } void Launcher::SettingsPage::slotShadowDistLimitToggled(bool checked) { shadowDistanceSpinBox->setEnabled(checked); fadeStartSpinBox->setEnabled(checked); } void Launcher::SettingsPage::slotDistantLandToggled(bool checked) { activeGridObjectPagingCheckBox->setEnabled(checked); objectPagingMinSizeComboBox->setEnabled(checked); } openmw-openmw-0.49.0/apps/launcher/settingspage.hpp000066400000000000000000000025031503074453300223720ustar00rootroot00000000000000#ifndef SETTINGSPAGE_H #define SETTINGSPAGE_H #include #include #include "ui_settingspage.h" namespace Config { class GameSettings; } namespace Launcher { class SettingsPage : public QWidget, private Ui::SettingsPage { Q_OBJECT public: explicit SettingsPage(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 slotPostProcessToggled(bool checked); void slotSkyBlendingToggled(bool checked); void slotShadowDistLimitToggled(bool checked); void slotDistantLandToggled(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); }; } #endif openmw-openmw-0.49.0/apps/launcher/textslotmsgbox.cpp000066400000000000000000000001721503074453300227760ustar00rootroot00000000000000#include "textslotmsgbox.hpp" void Launcher::TextSlotMsgBox::setTextSlot(const QString& string) { setText(string); } openmw-openmw-0.49.0/apps/launcher/textslotmsgbox.hpp000066400000000000000000000003721503074453300230050ustar00rootroot00000000000000#ifndef TEXT_SLOT_MSG_BOX #define TEXT_SLOT_MSG_BOX #include namespace Launcher { class TextSlotMsgBox : public QMessageBox { Q_OBJECT public slots: void setTextSlot(const QString& string); }; } #endif openmw-openmw-0.49.0/apps/launcher/ui/000077500000000000000000000000001503074453300176015ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/launcher/ui/datafilespage.ui000066400000000000000000000416161503074453300227410ustar00rootroot00000000000000 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 Qt::CustomContextMenu 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
openmw-openmw-0.49.0/apps/launcher/ui/directorypicker.ui000066400000000000000000000023141503074453300233420ustar00rootroot00000000000000 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() openmw-openmw-0.49.0/apps/launcher/ui/graphicspage.ui000066400000000000000000000162221503074453300226000ustar00rootroot00000000000000 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 openmw-openmw-0.49.0/apps/launcher/ui/importpage.ui000066400000000000000000000110141503074453300223040ustar00rootroot00000000000000 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 openmw-openmw-0.49.0/apps/launcher/ui/mainwindow.ui000066400000000000000000000115141503074453300223160ustar00rootroot00000000000000 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 openmw-openmw-0.49.0/apps/launcher/ui/settingspage.ui000066400000000000000000002245611503074453300226470ustar00rootroot00000000000000 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 <html><head/><body><p>This setting controls the behavior of factoring of Strength attribute into hand-to-hand damage: damage is multiplied by Strength value divided by 40.</p><p>Can apply to all actors or only to non-werewolf actors.</p></body></html> 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.250000000000000 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 openmw-openmw-0.49.0/apps/launcher/utils/000077500000000000000000000000001503074453300203245ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/launcher/utils/cellnameloader.cpp000066400000000000000000000042411503074453300240000ustar00rootroot00000000000000#include "cellnameloader.hpp" #include #include #include #include #include #include QSet CellNameLoader::getCellNames(const QStringList& contentPaths) { QSet cellNames; ESM::ESMReader esmReader; // Loop through all content files for (const QString& contentPath : contentPaths) { if (contentPath.endsWith(".omwscripts", Qt::CaseInsensitive)) continue; try { 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); } } // 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) { return recordName.toInt() == ESM::REC_CELL; } QString CellNameLoader::getCellName(ESM::ESMReader& esmReader) { ESM::Cell cell; bool isDeleted = false; cell.loadNameAndData(esmReader, isDeleted); return QString::fromStdString(cell.mName); } openmw-openmw-0.49.0/apps/launcher/utils/cellnameloader.hpp000066400000000000000000000020551503074453300240060ustar00rootroot00000000000000#ifndef OPENMW_CELLNAMELOADER_H #define OPENMW_CELLNAMELOADER_H #include #include #include namespace ESM { class ESMReader; struct Cell; } namespace ContentSelectorView { class ContentSelector; } 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(const QStringList& contentPaths); private: /** * Returns whether or not the given record is of type "Cell" * @param name The name associated with the record * @return whether or not the given record is of type "Cell" */ 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); }; #endif // OPENMW_CELLNAMELOADER_H openmw-openmw-0.49.0/apps/launcher/utils/lineedit.cpp000066400000000000000000000015721503074453300226320ustar00rootroot00000000000000#include "lineedit.hpp" LineEdit::LineEdit(QWidget* parent) : QLineEdit(parent) { setupClearButton(); } void LineEdit::setupClearButton() { mClearButton = new QToolButton(this); mClearButton->setIcon(QIcon::fromTheme("edit-clear")); mClearButton->setCursor(Qt::ArrowCursor); mClearButton->setAutoRaise(true); mClearButton->hide(); connect(mClearButton, &QToolButton::clicked, this, &LineEdit::clear); connect(this, &LineEdit::textChanged, this, &LineEdit::updateClearButton); } 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); } void LineEdit::updateClearButton(const QString& text) { mClearButton->setVisible(!text.isEmpty()); } openmw-openmw-0.49.0/apps/launcher/utils/lineedit.hpp000066400000000000000000000014631503074453300226360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (c) 2007 Trolltech ASA ** ** Use, modification and distribution is allowed without limitation, ** warranty, liability or support of any kind. ** ****************************************************************************/ #ifndef LINEEDIT_H #define LINEEDIT_H #include #include #include class QToolButton; class LineEdit : public QLineEdit { Q_OBJECT QString mPlaceholderText; public: LineEdit(QWidget* parent = nullptr); protected: void resizeEvent(QResizeEvent*) override; private slots: void updateClearButton(const QString& text); protected: QToolButton* mClearButton; void setupClearButton(); }; #endif // LIENEDIT_H openmw-openmw-0.49.0/apps/launcher/utils/openalutil.cpp000066400000000000000000000031501503074453300232030ustar00rootroot00000000000000#include #include #include "apps/openmw/mwsound/alext.h" #include "openalutil.hpp" #ifndef ALC_ALL_DEVICES_SPECIFIER #define ALC_ALL_DEVICES_SPECIFIER 0x1013 #endif std::vector Launcher::enumerateOpenALDevices() { 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; } std::vector Launcher::enumerateOpenALDevicesHrtf() { std::vector ret; ALCdevice* device = alcOpenDevice(nullptr); if (device) { if (alcIsExtensionPresent(device, "ALC_SOFT_HRTF")) { LPALCGETSTRINGISOFT alcGetStringiSOFT = nullptr; void* funcPtr = alcGetProcAddress(device, "alcGetStringiSOFT"); memcpy(&alcGetStringiSOFT, &funcPtr, sizeof(funcPtr)); 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) { const ALCchar* entry = alcGetStringiSOFT(device, ALC_HRTF_SPECIFIER_SOFT, i); if (strcmp(entry, "") == 0) break; ret.emplace_back(entry); } } alcCloseDevice(device); } return ret; } openmw-openmw-0.49.0/apps/launcher/utils/openalutil.hpp000066400000000000000000000002561503074453300232140ustar00rootroot00000000000000#include #include namespace Launcher { std::vector enumerateOpenALDevices(); std::vector enumerateOpenALDevicesHrtf(); } openmw-openmw-0.49.0/apps/launcher/utils/profilescombobox.cpp000066400000000000000000000050171503074453300244070ustar00rootroot00000000000000#include #include #include "profilescombobox.hpp" ProfilesComboBox::ProfilesComboBox(QWidget* parent) : ContentSelectorView::ComboBox(parent) { connect(this, qOverload(&ProfilesComboBox::activated), this, &ProfilesComboBox::slotIndexChangedByUser); setInsertPolicy(QComboBox::NoInsert); } void ProfilesComboBox::setEditEnabled(bool editable) { if (isEditable() == editable) return; if (!editable) { disconnect(lineEdit(), &QLineEdit::editingFinished, this, &ProfilesComboBox::slotEditingFinished); disconnect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::slotTextChanged); return setEditable(false); } // Reset the completer and validator setEditable(true); setValidator(mValidator); auto* edit = new ComboBoxLineEdit(this); setLineEdit(edit); setCompleter(nullptr); connect(lineEdit(), &QLineEdit::editingFinished, this, &ProfilesComboBox::slotEditingFinished); connect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::slotTextChanged); connect(lineEdit(), &QLineEdit::textChanged, this, &ProfilesComboBox::signalProfileTextChanged); } void ProfilesComboBox::slotTextChanged(const QString& text) { QPalette palette; palette.setColor(QPalette::Text, Qt::red); int index = findText(text); if (text.isEmpty() || (index != -1 && index != currentIndex())) { lineEdit()->setPalette(palette); } else { lineEdit()->setPalette(QApplication::palette()); } } void ProfilesComboBox::slotEditingFinished() { QString current = currentText(); QString previous = itemText(currentIndex()); if (currentIndex() == -1) return; if (current.isEmpty()) return; if (current == previous) return; if (findText(current) != -1) return; setItemText(currentIndex(), current); emit profileRenamed(previous, current); } void ProfilesComboBox::slotIndexChangedByUser(int index) { if (index == -1) return; emit signalProfileChanged(mOldProfile, currentText()); mOldProfile = currentText(); } 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)); } openmw-openmw-0.49.0/apps/launcher/utils/profilescombobox.hpp000066400000000000000000000021141503074453300244070ustar00rootroot00000000000000#ifndef PROFILESCOMBOBOX_HPP #define PROFILESCOMBOBOX_HPP #include "components/contentselector/view/combobox.hpp" #include "lineedit.hpp" #include class QString; class ProfilesComboBox : public ContentSelectorView::ComboBox { Q_OBJECT public: class ComboBoxLineEdit : public LineEdit { public: explicit ComboBoxLineEdit(QWidget* parent = nullptr); }; public: explicit ProfilesComboBox(QWidget* parent = nullptr); void setEditEnabled(bool editable); void setCurrentProfile(int index) { ComboBox::setCurrentIndex(index); mOldProfile = currentText(); } signals: 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); private slots: void slotEditingFinished(); void slotIndexChangedByUser(int index); void slotTextChanged(const QString& text); private: QString mOldProfile; }; #endif // PROFILESCOMBOBOX_HPP openmw-openmw-0.49.0/apps/launcher/utils/textinputdialog.cpp000066400000000000000000000036111503074453300242550ustar00rootroot00000000000000#include "textinputdialog.hpp" #include #include #include #include #include #include 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); auto* label = new QLabel(this); label->setText(text); // Line edit QValidator* validator = new QRegularExpressionValidator(QRegularExpression("^[a-zA-Z0-9_]*$"), this); mLineEdit = new LineEdit(this); mLineEdit->setValidator(validator); mLineEdit->setCompleter(nullptr); auto* dialogLayout = new QVBoxLayout(this); dialogLayout->addWidget(label); dialogLayout->addWidget(mLineEdit); dialogLayout->addWidget(mButtonBox); // Messageboxes on mac have no title #ifndef Q_OS_MAC setWindowTitle(title); #else Q_UNUSED(title); #endif setModal(true); connect(mButtonBox, &QDialogButtonBox::accepted, this, &TextInputDialog::accept); connect(mButtonBox, &QDialogButtonBox::rejected, this, &TextInputDialog::reject); } int Launcher::TextInputDialog::exec() { mLineEdit->clear(); mLineEdit->setFocus(); return QDialog::exec(); } void Launcher::TextInputDialog::setOkButtonEnabled(bool enabled) { QPushButton* okButton = mButtonBox->button(QDialogButtonBox::Ok); okButton->setEnabled(enabled); QPalette palette; palette.setColor(QPalette::Text, Qt::red); if (enabled) { mLineEdit->setPalette(QApplication::palette()); } else { // Existing profile name, make the text red mLineEdit->setPalette(palette); } } openmw-openmw-0.49.0/apps/launcher/utils/textinputdialog.hpp000066400000000000000000000011731503074453300242630ustar00rootroot00000000000000#ifndef TEXTINPUTDIALOG_HPP #define TEXTINPUTDIALOG_HPP #include #include "lineedit.hpp" class QDialogButtonBox; namespace Launcher { class TextInputDialog : public QDialog { Q_OBJECT public: explicit TextInputDialog(const QString& title, const QString& text, QWidget* parent = nullptr); ~TextInputDialog() override = default; inline LineEdit* lineEdit() { return mLineEdit; } void setOkButtonEnabled(bool enabled); int exec() override; private: QDialogButtonBox* mButtonBox; LineEdit* mLineEdit; }; } #endif // TEXTINPUTDIALOG_HPP openmw-openmw-0.49.0/apps/mwiniimporter/000077500000000000000000000000001503074453300202705ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/mwiniimporter/CMakeLists.txt000066400000000000000000000014111503074453300230250ustar00rootroot00000000000000set(MWINIIMPORT main.cpp importer.cpp ) set(MWINIIMPORT_HEADER importer.hpp ) source_group(launcher FILES ${MWINIIMPORT} ${MWINIIMPORT_HEADER}) openmw_add_executable(openmw-iniimporter ${MWINIIMPORT} ) target_link_libraries(openmw-iniimporter Boost::program_options components ) if (WIN32) INSTALL(TARGETS openmw-iniimporter RUNTIME DESTINATION ".") endif(WIN32) if (MINGW) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -municode") endif() if (BUILD_WITH_CODE_COVERAGE) 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() openmw-openmw-0.49.0/apps/mwiniimporter/importer.cpp000066400000000000000000000774621503074453300226550ustar00rootroot00000000000000#include "importer.hpp" #include #include #include #include #include #include #include #include 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[] = { // light "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", // 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", // 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:UnderwaterColorWeight", // pixelwater "PixelWater:SurfaceFPS", "PixelWater:TileCount", "PixelWater:Resolution", // fonts "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", // 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", // 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", // 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", // movies "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: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 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 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: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", // 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: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", // werewolf (Bloodmoon) "General:Werewolf FOV", 0 }; 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++) { mMergeFallback.emplace_back(fallback[i]); } } void MwIniImporter::setVerbose(bool verbose) { mVerbose = verbose; } 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; std::ifstream file(filename); ToUTF8::Utf8Encoder encoder(mEncoding); std::string line; while (std::getline(file, line)) { std::string_view utf8 = encoder.getUtf8(line); // unify Unix-style and Windows file ending if (!(utf8.empty()) && (utf8[utf8.length() - 1]) == '\r') { utf8 = utf8.substr(0, utf8.length() - 1); } if (utf8.empty()) { continue; } 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 = utf8.substr(1, utf8.find(']') - 1); continue; } int comment_pos = static_cast(utf8.find(';')); if (comment_pos > 0) { utf8 = utf8.substr(0, comment_pos); } int pos = static_cast(utf8.find('=')); if (pos < 1) { continue; } 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; } 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 std::filesystem::path& filename) { std::cout << "load cfg file: " << Files::pathToUnicodeString(filename) << std::endl; MwIniImporter::multistrmap map; std::ifstream file(filename); std::string line; while (std::getline(file, line)) { // we cant say comment by only looking at first char anymore int comment_pos = static_cast(line.find('#')); if (comment_pos > 0) { line = line.substr(0, comment_pos); } if (line.empty()) { continue; } int pos = static_cast(line.find('=')); if (pos < 1) { continue; } 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())); } map[key].push_back(value); } return map; } 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) { cfg.erase(it->first); insertMultistrmap(cfg, it->first, *vc); } } } } 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) { std::string value(*it); 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 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 { std::vector archives; // Search archives listed in ini file 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()) { break; } 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())); // 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 (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) { auto iter = std::find_if( source.begin(), source.end(), [&element](std::pair>& sourceElement) { return sourceElement.first == element; }); if (iter != source.end()) { auto foundElement = std::move(*iter); source.erase(iter); for (auto name : foundElement.second) { MwIniImporter::dependencySortStep(name, source, result); } result.push_back(std::move(foundElement.first)); } } std::vector MwIniImporter::dependencySort(MwIniImporter::dependencyList source) { std::vector result; while (!source.empty()) { MwIniImporter::dependencySortStep(source.begin()->first, source, result); } return result; } 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); }); } void MwIniImporter::addPaths(std::vector& output, std::vector input) { for (auto& path : input) { if (path.front() == '"') { // Drop first and last characters - quotation marks path = path.substr(1, path.size() - 2); } output.emplace_back(Files::pathFromUnicodeString(path)); } } void MwIniImporter::importGameFiles( multistrmap& cfg, const multistrmap& ini, const std::filesystem::path& iniFilename) const { std::vector> contentFiles; std::time_t defaultTime = 0; ToUTF8::Utf8Encoder encoder(mEncoding); std::vector dataPaths; if (cfg.count("data")) addPaths(dataPaths, cfg["data"]); if (cfg.count("data-local")) addPaths(dataPaths, cfg["data-local"]); dataPaths.push_back(iniFilename.parent_path() /= "Data Files"); auto it = ini.begin(); for (int i = 0; it != ini.end(); i++) { std::string gameFile("Game Files:GameFile" + std::to_string(i)); it = ini.find(gameFile); if (it == ini.end()) break; for (const std::string& file : it->second) { if (Misc::StringUtils::ciEndsWith(file, "esm") || Misc::StringUtils::ciEndsWith(file, "esp")) { bool found = false; for (auto& dataPath : dataPaths) { std::filesystem::path path = dataPath / file; std::time_t time = lastWriteTime(path, defaultTime); if (time != defaultTime) { contentFiles.emplace_back(time, std::move(path)); found = true; break; } } if (!found) std::cout << "Warning: " << file << " not found, ignoring" << std::endl; } } } cfg.erase("content"); cfg.insert(std::make_pair("content", std::vector())); // sort by timestamp sort(contentFiles.begin(), contentFiles.end()); MwIniImporter::dependencyList unsortedFiles; ESM::ESMReader reader; reader.setEncoder(&encoder); for (auto& file : contentFiles) { reader.open(file.second); std::vector dependencies; for (auto& gameFile : reader.getGameFiles()) { dependencies.push_back(gameFile.name); } unsortedFiles.emplace_back(Files::pathToUnicodeString(reader.getName().filename()), dependencies); reader.close(); } auto sortedFiles = dependencySort(std::move(unsortedFiles)); // hard-coded dependency Morrowind - Tribunal - Bloodmoon if (findString(sortedFiles, "Morrowind.esm") != sortedFiles.end()) { 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); if (bloodmoonIndex < tribunalIndex) tribunalIndex++; sortedFiles.insert(bloodmoonIter, *tribunalIter); sortedFiles.erase(sortedFiles.begin() + tribunalIndex); } } for (auto& file : sortedFiles) cfg["content"].push_back(file); } void MwIniImporter::writeToFile(std::ostream& out, const multistrmap& cfg) { 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) { mEncoding = encoding; } std::time_t MwIniImporter::lastWriteTime(const std::filesystem::path& filename, std::time_t defaultTime) { std::time_t writeTime(defaultTime); if (std::filesystem::exists(filename)) { std::filesystem::path resolved = std::filesystem::canonical(filename); const auto time = std::filesystem::last_write_time(resolved); writeTime = Misc::toTimeT(time); // print timestamp const auto str = Misc::fileTimeToString(time, "%x %X"); if (!str.empty()) std::cout << "content file: " << resolved << " timestamp = (" << writeTime << ") " << str << std::endl; } return writeTime; } openmw-openmw-0.49.0/apps/mwiniimporter/importer.hpp000066400000000000000000000037551503074453300226540ustar00rootroot00000000000000#ifndef MWINIIMPORTER_IMPORTER #define MWINIIMPORTER_IMPORTER 1 #include #include #include #include #include #include #include class MwIniImporter { public: typedef std::map strmap; typedef std::map> multistrmap; typedef std::vector>> dependencyList; MwIniImporter(); 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); 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); /// \return file's "last modified time", used in original MW to determine plug-in load order static std::time_t lastWriteTime(const std::filesystem::path& filename, std::time_t defaultTime); bool mVerbose; strmap mMergeMap; std::vector mMergeFallback; ToUTF8::FromType mEncoding; }; #endif openmw-openmw-0.49.0/apps/mwiniimporter/main.cpp000066400000000000000000000133451503074453300217260ustar00rootroot00000000000000#include "importer.hpp" #include #include #include #include #include #include namespace bpo = boost::program_options; namespace sfs = std::filesystem; #ifndef _WIN32 int main(int argc, char* argv[]) { #else // Include on Windows only #include #include class utf8argv { public: utf8argv(int argc, wchar_t* wargv[]) { args.reserve(argc); argv = new const char*[argc]; 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); } private: utf8argv(const utf8argv&); utf8argv& operator=(const utf8argv&); const char** argv; std::vector args; }; /* The only way to pass Unicode on Winodws with CLI is to use wide characters interface which presents UTF-16 encoding. The rest of OpenMW application stack assumes UTF-8 encoding, therefore this conversion. */ int wmain(int argc, wchar_t* wargv[]) { utf8argv converter(argc, wargv); char** argv = converter.get(); #endif try { bpo::options_description desc("Syntax: openmw-iniimporter inifile configfile\nAllowed options"); bpo::positional_options_description p_desc; 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::store(parsed, vm); if (vm.count("help") || !vm.count("ini") || !vm.count("cfg")) { std::cout << desc; return 0; } bpo::notify(vm); 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::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 (!std::filesystem::exists(iniFile)) { std::cerr << "ini file does not exist" << std::endl; return -3; } if (!std::filesystem::exists(cfgFile)) std::cerr << "cfg file does not exist" << std::endl; MwIniImporter importer; importer.setVerbose(vm.count("verbose") != 0); // Font encoding settings std::string encoding(vm["encoding"].as()); importer.setInputEncoding(ToUTF8::calculateEncoding(encoding)); 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")) { importer.importGameFiles(cfg, ini, iniFile); } if (!vm.count("no-archives")) { importer.importArchives(cfg, ini); } std::cout << "write to: " << Files::pathToUnicodeString(outputFile) << std::endl; std::ofstream file(outputFile); importer.writeToFile(file, cfg); } catch (std::exception& e) { std::cerr << "ERROR: " << e.what() << std::endl; } return 0; } openmw-openmw-0.49.0/apps/navmeshtool/000077500000000000000000000000001503074453300177225ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/navmeshtool/CMakeLists.txt000066400000000000000000000016151503074453300224650ustar00rootroot00000000000000set(NAVMESHTOOL_LIB worldspacedata.cpp navmesh.cpp ) source_group(apps\\navmeshtool FILES ${NAVMESHTOOL_LIB} main.cpp) add_library(openmw-navmeshtool-lib STATIC ${NAVMESHTOOL_LIB}) if (ANDROID) add_library(openmw-navmeshtool SHARED main.cpp) else() openmw_add_executable(openmw-navmeshtool main.cpp) 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() openmw-openmw-0.49.0/apps/navmeshtool/main.cpp000066400000000000000000000262271503074453300213630ustar00rootroot00000000000000#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() { 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.processPaths(variables, std::filesystem::current_path()); 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(Debug::getRecastMaxLogLevel()); 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); } openmw-openmw-0.49.0/apps/navmeshtool/navmesh.cpp000066400000000000000000000304561503074453300220770ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/navmeshtool/navmesh.hpp000066400000000000000000000011551503074453300220760ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/navmeshtool/worldspacedata.cpp000066400000000000000000000404161503074453300234300ustar00rootroot00000000000000#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) { VFS::Path::Normalized 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; } } openmw-openmw-0.49.0/apps/navmeshtool/worldspacedata.hpp000066400000000000000000000057321503074453300234370ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/niftest/000077500000000000000000000000001503074453300170375ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/niftest/CMakeLists.txt000066400000000000000000000010051503074453300215730ustar00rootroot00000000000000set(NIFTEST niftest.cpp ) source_group(components\\nif\\tests FILES ${NIFTEST}) # Main executable openmw_add_executable(niftest ${NIFTEST} ) target_link_libraries(niftest components ) if (BUILD_WITH_CODE_COVERAGE) target_compile_options(niftest PRIVATE --coverage) target_link_libraries(niftest gcov) endif() if (WIN32) install(TARGETS niftest RUNTIME DESTINATION ".") endif() if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC) target_precompile_headers(niftest PRIVATE ) endif() openmw-openmw-0.49.0/apps/niftest/niftest.cpp000066400000000000000000000244421503074453300212250ustar00rootroot00000000000000/// 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 // Create local aliases for brevity namespace bpo = boost::program_options; enum class FileType { BSA, BA2, BGEM, BGSM, NIF, KF, BTO, BTR, RDT, PSA, Unknown, }; enum class FileClass { Archive, Material, NIF, Unknown, }; 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; } std::unique_ptr makeArchive(const std::filesystem::path& path) { if (isBSA(path)) return VFS::makeBsaArchive(path); if (std::filesystem::is_directory(path)) return std::make_unique(path); return nullptr; } bool readFile( const std::filesystem::path& source, const std::filesystem::path& path, const VFS::Manager* vfs, bool quiet) { 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(VFS::Path::Normalized(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 Can not read a bsa file inside of a bsa file. void readVFS(std::unique_ptr&& archive, const std::filesystem::path& archivePath, bool quiet) { if (archive == nullptr) return; 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()) { readFile(archivePath, name.value(), &vfs, quiet); } 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) { try { readVFS(VFS::makeBsaArchive(file.second), file.second, quiet); } catch (const std::exception& e) { std::cerr << "Failed to read archive file '" << Files::pathToUnicodeString(file.second) << "': " << e.what() << std::endl; } } } } } bool parseOptions(int argc, char** argv, Files::PathContainer& files, Files::PathContainer& archives, bool& writeDebugLog, bool& quiet) { 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::store(valid_opts, variables); bpo::notify(variables); 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 = 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) { std::cout << "Error parsing arguments: " << e.what() << "\n\n" << desc << std::endl; return false; } std::cout << "No input files or directories specified!" << std::endl; std::cout << desc << std::endl; return false; } int main(int argc, char** argv) { Files::PathContainer files, sources; bool writeDebugLog = false; bool quiet = false; if (!parseOptions(argc, argv, files, sources, writeDebugLog, quiet)) return 1; Nif::Reader::setLoadUnsupportedFiles(true); Nif::Reader::setWriteNifDebugLog(writeDebugLog); std::unique_ptr vfs; if (!sources.empty()) { 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 { const bool isFile = readFile({}, path, vfs.get(), quiet); if (!isFile) { 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 << "Failed to read '" << pathStr << "': " << e.what() << std::endl; } } return 0; } openmw-openmw-0.49.0/apps/opencs/000077500000000000000000000000001503074453300166525ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/CMakeLists.txt000066400000000000000000000224731503074453300214220ustar00rootroot00000000000000set (OPENCS_SRC ) opencs_units (. editor) opencs_units (model/doc document operation saving documentmanager loader runner operationholder ) opencs_units (model/doc savingstate savingstages messages ) opencs_hdrs (model/doc state ) opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel landtexturetableproxymodel actoradapter idcollection ) opencs_units (model/world universalid record commands columnbase columnimp scriptcontext cell refidcollection 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 (model/world columnimp disabletag idcollection collection info subcellcollection ) opencs_units (model/tools tools reportmodel mergeoperation ) 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 effectlistcheck ) opencs_hdrs (model/tools mergestate ) opencs_units (view/doc viewmanager view operations operation subview startup filedialog newgame filewidget adjusterwidget loader globaldebugprofilemenu runlogsubview sizehint ) opencs_units (view/doc subviewfactory ) opencs_hdrs (view/doc subviewfactoryimp ) opencs_units (view/world table tablesubview scriptsubview util regionmapsubview tablebottombox creator genericcreator globalcreator cellcreator pathgridcreator referenceablecreator startscriptcreator referencecreator scenesubview infocreator scriptedit dialoguesubview previewsubview regionmap dragrecordtable nestedtable dialoguespinbox recordbuttonbar tableeditidaction scripterrortable extendedcommandconfigurator bodypartcreator landcreator tableheadermouseeventhandler ) opencs_units (view/world subviews enumdelegate vartypedelegate recordstatusdelegate idtypedelegate datadisplaydelegate scripthighlighter idvalidator dialoguecreator idcompletiondelegate colordelegate dragdroputils ) opencs_units (view/widget scenetoolbar scenetool scenetoolmode pushbutton scenetooltoggle scenetoolrun modebutton scenetooltoggle2 scenetooltexturebrush scenetoolshapebrush completerpopup coloreditor colorpickerpopup droplineedit ) opencs_units (view/render scenewidget worldspacewidget pagedworldspacewidget unpagedworldspacewidget previewwidget editmode instancemode instanceselectionmode instancemovemode orbitcameramode pathgridmode selectionmode pathgridselectionmode cameracontroller cellwater terraintexturemode actor terrainselection terrainshapemode brushdraw commands ) opencs_units (view/render lighting lightingday lightingnight lightingbright object cell terrainstorage tagbase cellarrow cellmarker cellborder pathgrid ) opencs_hdrs (view/render mask ) opencs_units (view/tools reportsubview reporttable searchsubview searchbox merge ) opencs_units (view/tools subviews ) opencs_units (view/prefs dialogue pagebase page keybindingpage contextmenulist ) opencs_units (model/prefs state setting intsetting doublesetting boolsetting enumsetting coloursetting shortcut shortcuteventhandler shortcutmanager shortcutsetting modifiersetting stringsetting subcategory ) opencs_units (model/prefs category ) opencs_units (model/filter unarynode narynode leafnode booleannode parser andnode ornode notnode textnode valuenode ) opencs_units (view/filter filterbox recordfilterbox editwidget ) set (OPENCS_US ) set (OPENCS_RES ${CMAKE_SOURCE_DIR}/files/opencs/resources.qrc ${CMAKE_SOURCE_DIR}/files/launcher/launcher.qrc ) 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) qt_add_resources(OPENCS_RES_SRC ${OPENCS_RES}) if(APPLE) set (OPENCS_MAC_ICON "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs.icns") 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() set (OPENCS_MAC_ICON "") set (OPENCS_CFG "") set (OPENCS_DEFAULT_FILTERS_FILE "") set (OPENCS_OPENMW_CFG "") endif(APPLE) add_library(openmw-cs-lib STATIC ${OPENCS_SRC} ${OPENCS_UI_HDR} ${OPENCS_MOC_SRC} ${OPENCS_RES_SRC} ) 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) set_property(TARGET openmw-cs PROPERTY AUTOMOC ON) set_property(TARGET openmw-cs PROPERTY AUTOUIC_SEARCH_PATHS ui) set_property(TARGET openmw-cs PROPERTY AUTOUIC ON) 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_RESOURCES_ROOT ${OPENCS_BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) set_target_properties(openmw-cs PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${OpenMW_BINARY_DIR}" OUTPUT_NAME ${OPENCS_BUNDLE_NAME} MACOSX_BUNDLE_ICON_FILE "openmw-cs.icns" MACOSX_BUNDLE_BUNDLE_NAME "OpenMW-CS" MACOSX_BUNDLE_GUI_IDENTIFIER "org.openmw.opencs" MACOSX_BUNDLE_SHORT_VERSION_STRING ${OPENMW_VERSION} MACOSX_BUNDLE_BUNDLE_VERSION ${OPENMW_VERSION} MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/files/mac/openmw-cs-Info.plist.in" ) set_source_files_properties(${OPENCS_MAC_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(${OPENCS_CFG} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(${OPENCS_DEFAULT_FILTERS_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/resources) set_source_files_properties(${OPENCS_OPENMW_CFG} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) add_custom_command(TARGET openmw-cs POST_BUILD COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${OPENCS_BUNDLE_RESOURCES_DIR}/resources") endif() 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`. # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGVIEWER_LIBRARIES} ${OSGFX_LIBRARIES} ${OSGGA_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSGTEXT_LIBRARIES} ${OSG_LIBRARIES} ${EXTERN_OSGQT_LIBRARY} Boost::program_options components_qt ) if (QT_VERSION_MAJOR VERSION_EQUAL 6) target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::OpenGLWidgets Qt::Svg) else() target_link_libraries(openmw-cs-lib Qt::Widgets Qt::Core Qt::Network Qt::OpenGL Qt::Svg) endif() if (WIN32) 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) if (multi_config) SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}/$") else () SET(INSTALL_SOURCE "${OpenMW_BINARY_DIR}") endif () INSTALL(FILES "${INSTALL_SOURCE}/defaults-cs.bin" DESTINATION ".") 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(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) set_property(TARGET openmw-cs-lib PROPERTY AUTOUIC_SEARCH_PATHS ui) set_property(TARGET openmw-cs-lib PROPERTY AUTOUIC 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() openmw-openmw-0.49.0/apps/opencs/editor.cpp000066400000000000000000000335371503074453300206570ustar00rootroot00000000000000#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 "view/doc/viewmanager.hpp" 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(); mViewManager = new CSVDoc::ViewManager(mDocumentManager); if (argc > 1) { mFileToLoad = argv[1]; mDataDirs = config.first; } NifOsg::Loader::setShowMarkers(true); mDocumentManager.setFileData(config.first, config.second); mNewGame.setLocalData(mLocal); mFileDialog.setLocalData(mLocal); mMerge.setLocalData(mLocal); 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(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(&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, &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, &CSVDoc::NewGameDialogue::createRequest, this, &Editor::createNewGame); connect(&mNewGame, &CSVDoc::NewGameDialogue::cancelCreateGame, this, &Editor::cancelCreateGame); } CS::Editor::~Editor() { delete mViewManager; mLockFile.unlock(); mPidFile.close(); if (mServer && std::filesystem::exists(mPid)) std::filesystem::remove(mPid); } 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"); 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(Fallback::FallbackMap(), "") ->multitoken() ->composing(), "fallback values"); 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(); mDocumentManager.setEncoding(ToUTF8::calculateEncoding(mEncodingName)); mFileDialog.setEncoding(QString::fromUtf8(mEncodingName.c_str())); 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. Files::PathContainer dataDirs, dataLocal; if (!variables["data"].empty()) { dataDirs = asPathContainer(variables["data"].as()); } 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.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.exec(); QApplication::exit(1); } dataDirs.insert(dataDirs.end(), dataLocal.begin(), dataLocal.end()); 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>()); } void CS::Editor::createGame() { mStartup.hide(); if (mNewGame.isHidden()) mNewGame.show(); mNewGame.raise(); mNewGame.activateWindow(); } void CS::Editor::cancelCreateGame() { if (!mDocumentManager.isEmpty()) return; mNewGame.hide(); if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::createAddon() { mStartup.hide(); mFileDialog.clearFiles(); readConfig(/*quiet*/ true); mFileDialog.showDialog(CSVDoc::ContentAction_New); } void CS::Editor::cancelFileDialog() { if (!mDocumentManager.isEmpty()) return; mFileDialog.hide(); if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::loadDocument() { mStartup.hide(); mFileDialog.clearFiles(); readConfig(/*quiet*/ true); mFileDialog.showDialog(CSVDoc::ContentAction_Edit); } void CS::Editor::openFiles( const std::filesystem::path& savePath, const std::vector& discoveredFiles) { std::vector files; if (discoveredFiles.empty()) { for (const QString& path : mFileDialog.selectedFilePaths()) { files.emplace_back(Files::pathFromQString(path)); } } else { files = discoveredFiles; } mDocumentManager.addDocument(files, savePath, false); mFileDialog.hide(); } void CS::Editor::createNewFile(const std::filesystem::path& savePath) { std::vector files; for (const QString& path : mFileDialog.selectedFilePaths()) { files.emplace_back(Files::pathFromQString(path)); } files.push_back(savePath); mDocumentManager.addDocument(files, savePath, true); mFileDialog.hide(); } void CS::Editor::createNewGame(const std::filesystem::path& file) { std::vector files; files.push_back(file); mDocumentManager.addDocument(files, file, true); mNewGame.hide(); } void CS::Editor::showStartup() { if (mStartup.isHidden()) mStartup.show(); mStartup.raise(); mStartup.activateWindow(); } void CS::Editor::showSettings() { if (mSettings.isHidden()) mSettings.show(); mSettings.move(QCursor::pos()); mSettings.raise(); mSettings.activateWindow(); } bool CS::Editor::makeIPCServer() { try { bool pidExists = std::filesystem::exists(mPid); mPidFile.open(mPid); if (!mLockFile.tryLock()) { Log(Debug::Error) << "Error: OpenMW-CS is already running."; return false; } #ifdef _WIN32 mPidFile << GetCurrentProcessId() << std::endl; #else mPidFile << getpid() << std::endl; #endif mServer = new QLocalServer(this); if (pidExists) { // hack to get the temp directory path mServer->listen("dummy"); QString fullPath = mServer->fullServerName(); mServer->close(); fullPath.remove(QRegularExpression("dummy$")); fullPath += mIpcServerName; 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(path)) Log(Debug::Error) << "Error: can not remove stale connection file."; } } } catch (const std::exception& e) { Log(Debug::Error) << "Error: " << e.what(); return false; } if (mServer->listen(mIpcServerName)) { connect(mServer, &QLocalServer::newConnection, this, &Editor::showStartup); return true; } mServer->close(); mServer = nullptr; return false; } void CS::Editor::connectToIPCServer() { mClientSocket = new QLocalSocket(this); mClientSocket->connectToServer(mIpcServerName); mClientSocket->close(); } int CS::Editor::run() { if (mLocal.empty()) return 1; Misc::Rng::init(); QApplication::setQuitOnLastWindowClosed(true); if (mFileToLoad.empty()) { mStartup.show(); } else { ESM::ESMReader fileReader; ToUTF8::Utf8Encoder encoder(ToUTF8::calculateEncoding(mEncodingName)); fileReader.setEncoder(&encoder); fileReader.open(mFileToLoad); std::vector discoveredFiles; for (const auto& item : fileReader.getGameFiles()) { for (const auto& path : mDataDirs) { if (auto masterPath = path / item.name; std::filesystem::exists(masterPath)) { discoveredFiles.emplace_back(std::move(masterPath)); break; } } } discoveredFiles.push_back(mFileToLoad); const auto extension = Files::pathToQString(mFileToLoad.extension()).toLower(); if (extension == ".esm") { mFileToLoad.replace_extension(".omwgame"); mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false); } else if (extension == ".esp") { mFileToLoad.replace_extension(".omwaddon"); mDocumentManager.addDocument(discoveredFiles, mFileToLoad, false); } else { openFiles(mFileToLoad, discoveredFiles); } } return QApplication::exec(); } void CS::Editor::documentAdded(CSMDoc::Document* document) { mViewManager->addView(document); } void CS::Editor::documentAboutToBeRemoved(CSMDoc::Document* document) { if (mMerge.getDocument() == document) mMerge.cancel(); } void CS::Editor::lastDocumentDeleted() { QApplication::quit(); } void CS::Editor::mergeDocument(CSMDoc::Document* document) { mMerge.configure(document); mMerge.show(); mMerge.raise(); mMerge.activateWindow(); } openmw-openmw-0.49.0/apps/opencs/editor.hpp000066400000000000000000000057361503074453300206640ustar00rootroot00000000000000#ifndef CS_EDITOR_H #define CS_EDITOR_H #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN #include #endif #include #include "model/doc/documentmanager.hpp" #include "model/prefs/state.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; 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; }; } #endif openmw-openmw-0.49.0/apps/opencs/main.cpp000066400000000000000000000037011503074453300203030ustar00rootroot00000000000000#include "editor.hpp" #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 QEvent; class QObject; 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[]) { Platform::init(); #ifdef Q_OS_MAC setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif Q_INIT_RESOURCE(resources); #ifdef WIN32 Q_INIT_RESOURCE(dark); #endif qRegisterMetaType("std::string"); qRegisterMetaType("CSMWorld::UniversalId"); qRegisterMetaType("CSMWorld::DisableTag"); qRegisterMetaType("CSMDoc::Message"); setQSurfaceFormat(); QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); Platform::Application application(argc, argv); application.setWindowIcon(QIcon(":openmw-cs")); CS::Editor editor(argc, argv); #ifdef __linux__ setlocale(LC_NUMERIC, "C"); #endif if (!editor.makeIPCServer()) { editor.connectToIPCServer(); return 0; } return editor.run(); } int main(int argc, char* argv[]) { return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW-CS"); } openmw-openmw-0.49.0/apps/opencs/model/000077500000000000000000000000001503074453300177525ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/doc/000077500000000000000000000000001503074453300205175ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/doc/document.cpp000066400000000000000000000350331503074453300230450ustar00rootroot00000000000000#include "document.hpp" #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 "../world/defaultgmsts.hpp" #ifndef Q_MOC_RUN #include #endif namespace CSMWorld { class IdCompletionManager; } void CSMDoc::Document::addGmsts() { for (size_t i = 0; i < CSMWorld::DefaultGmsts::FloatCount; ++i) { ESM::GameSetting 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) { ESM::GameSetting 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) { ESM::GameSetting 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) { ESM::GameSetting gmst; gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalFloats[i]); gmst.blank(); gmst.mValue.setType(ESM::VT_Float); addOptionalGmst(gmst); } for (size_t i = 0; i < CSMWorld::DefaultGmsts::OptionalIntCount; ++i) { ESM::GameSetting gmst; gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalInts[i]); gmst.blank(); gmst.mValue.setType(ESM::VT_Int); addOptionalGmst(gmst); } for (size_t i = 0; i < CSMWorld::DefaultGmsts::OptionalStringCount; ++i) { ESM::GameSetting gmst; gmst.mId = ESM::RefId::stringRefId(CSMWorld::DefaultGmsts::OptionalStrings[i]); gmst.blank(); gmst.mValue.setType(ESM::VT_String); gmst.mValue.setString(""); addOptionalGmst(gmst); } } void CSMDoc::Document::addOptionalGlobals() { static constexpr std::string_view globals[] = { "DaysPassed", "PCWerewolf", "PCYear", }; for (std::size_t i = 0; i < std::size(globals); ++i) { ESM::Global global; global.mId = ESM::RefId::stringRefId(globals[i]); global.blank(); global.mValue.setType(ESM::VT_Long); if (i == 0) global.mValue.setInteger(1); // dayspassed starts counting at 1 addOptionalGlobal(global); } } void CSMDoc::Document::addOptionalMagicEffects() { for (int i = ESM::MagicEffect::SummonFabricant; i <= ESM::MagicEffect::SummonCreature05; ++i) { ESM::MagicEffect effect; effect.mIndex = i; effect.mId = ESM::MagicEffect::indexToRefId(i); effect.blank(); addOptionalMagicEffect(effect); } } void CSMDoc::Document::addOptionalGmst(const ESM::GameSetting& gmst) { if (getData().getGmsts().searchId(gmst.mId) == -1) { 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) { if (getData().getGlobals().searchId(global.mId) == -1) { 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) { if (getData().getMagicEffects().searchId(magicEffect.mId) == -1) { 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 constexpr std::string_view globals[] = { "Day", "DaysPassed", "GameHour", "Month", "PCRace", "PCVampire", "PCWerewolf", "PCYear", }; for (std::size_t i = 0; i < std::size(globals); ++i) { ESM::Global record; 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); getData().getGlobals().add(record); } addGmsts(); for (int i = 0; i < ESM::Skill::Length; ++i) { ESM::Skill record; record.mId = *ESM::Skill::indexToRefId(i).getIf(); record.blank(); getData().getSkills().add(record); } static constexpr std::string_view voices[] = { "Intruder", "Attack", "Hello", "Thief", "Alarm", "Idle", "Flee", "Hit", }; for (const std::string_view voice : voices) { ESM::Dialogue record; record.mId = ESM::RefId::stringRefId(voice); record.mStringId = voice; record.mType = ESM::Dialogue::Voice; record.blank(); getData().getTopics().add(record); } static constexpr std::string_view greetings[] = { "Greeting 0", "Greeting 1", "Greeting 2", "Greeting 3", "Greeting 4", "Greeting 5", "Greeting 6", "Greeting 7", "Greeting 8", "Greeting 9", }; for (const std::string_view greeting : greetings) { ESM::Dialogue record; record.mId = ESM::RefId::stringRefId(greeting); record.mStringId = greeting; record.mType = ESM::Dialogue::Greeting; record.blank(); getData().getTopics().add(record); } static constexpr std::string_view persuasions[] = { "Intimidate Success", "Intimidate Fail", "Service Refusal", "Admire Success", "Taunt Success", "Bribe Success", "Info Refusal", "Admire Fail", "Taunt Fail", "Bribe Fail", }; for (const std::string_view persuasion : persuasions) { ESM::Dialogue record; record.mId = ESM::RefId::stringRefId(persuasion); record.mStringId = persuasion; record.mType = ESM::Dialogue::Persuasion; record.blank(); getData().getTopics().add(record); } for (int i = 0; i < ESM::MagicEffect::Length; ++i) { ESM::MagicEffect record; record.mIndex = i; record.mId = ESM::MagicEffect::indexToRefId(i); record.blank(); getData().getMagicEffects().add(record); } } 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 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"); if (mNew || !std::filesystem::exists(mProjectPath)) { auto filtersPath = configuration.getUserDataPath() / "defaultfilters"; std::ofstream destination(mProjectPath, std::ios::out | std::ios::binary); if (!destination.is_open()) throw std::runtime_error("Can not create project file: " + Files::pathToUnicodeString(mProjectPath)); destination.exceptions(std::ios::failbit | std::ios::badbit); if (!std::filesystem::exists(filtersPath)) filtersPath = mResDir / "defaultfilters"; std::ifstream source(filtersPath, std::ios::in | std::ios::binary); if (!source.is_open()) throw std::runtime_error("Can not read filters file: " + Files::pathToUnicodeString(filtersPath)); source.exceptions(std::ios::failbit | std::ios::badbit); destination << source.rdbuf(); } if (mNew) { if (mContentFiles.size() == 1) createBase(); } addOptionalGmsts(); addOptionalGlobals(); addOptionalMagicEffects(); connect(&mUndoStack, &QUndoStack::cleanChanged, this, &Document::modificationStateChanged); 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, &OperationHolder::progress, this, qOverload(&Document::progress)); connect(&mSaving, &OperationHolder::done, this, &Document::operationDone2); connect(&mSaving, &OperationHolder::reportMessage, this, &Document::reportMessage); connect(&mRunner, &Runner::runStateChanged, this, &Document::runStateChanged); } QUndoStack& CSMDoc::Document::getUndoStack() { return mUndoStack; } int CSMDoc::Document::getState() const { int state = 0; if (!mUndoStack.isClean() || mDirty) state |= State_Modified; if (mSaving.isRunning()) state |= State_Locked | State_Saving | State_Operation; if (mRunner.isRunning()) state |= State_Locked | State_Running; if (int operations = mTools.getRunningOperations()) state |= State_Locked | State_Operation | operations; return state; } const std::filesystem::path& CSMDoc::Document::getResourceDir() const { return mResDir; } const std::filesystem::path& CSMDoc::Document::getSavePath() const { return mSavePath; } const std::filesystem::path& CSMDoc::Document::getProjectPath() const { return mProjectPath; } const std::vector& CSMDoc::Document::getContentFiles() const { return mContentFiles; } bool CSMDoc::Document::isNew() const { return mNew; } void CSMDoc::Document::save() { if (mSaving.isRunning()) throw std::logic_error("Failed to initiate save, because a save operation is already running."); mSaving.start(); emit stateChanged(getState(), this); } CSMWorld::UniversalId CSMDoc::Document::verify(const CSMWorld::UniversalId& reportId) { 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) { mTools.runSearch(searchId, search); emit stateChanged(getState(), this); } void CSMDoc::Document::runMerge(std::unique_ptr target) { mTools.runMerge(std::move(target)); emit stateChanged(getState(), this); } void CSMDoc::Document::abortOperation(int type) { if (type == State_Saving) mSaving.abort(); else mTools.abortOperation(type); } void CSMDoc::Document::modificationStateChanged(bool clean) { emit stateChanged(getState(), this); } 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) { if (type == CSMDoc::State_Saving && !failed) mDirty = false; emit stateChanged(getState(), this); } const CSMWorld::Data& CSMDoc::Document::getData() const { return mData; } CSMWorld::Data& CSMDoc::Document::getData() { return mData; } CSMTools::ReportModel* CSMDoc::Document::getReport(const CSMWorld::UniversalId& id) { return mTools.getReport(id); } void CSMDoc::Document::startRunning(const std::string& profile, const std::string& startupInstruction) { std::vector contentFiles; for (const auto& mContentFile : mContentFiles) { contentFiles.emplace_back(mContentFile.filename()); } mRunner.configure(getData().getDebugProfiles().getRecord(ESM::RefId::stringRefId(profile)).get(), contentFiles, startupInstruction); int state = getState(); if (state & State_Modified) { // need to save first mRunner.start(true); new SaveWatcher(&mRunner, &mSaving); // no, that is not a memory leak. Qt is weird. if (!(state & State_Saving)) save(); } else mRunner.start(); } void CSMDoc::Document::stopRunning() { mRunner.stop(); } QTextDocument* CSMDoc::Document::getRunLog() { return mRunner.getLog(); } void CSMDoc::Document::runStateChanged() { emit stateChanged(getState(), this); } void CSMDoc::Document::progress(int current, int max, int type) { emit progress(current, max, type, 1, this); } CSMWorld::IdCompletionManager& CSMDoc::Document::getIdCompletionManager() { return mIdCompletionManager; } void CSMDoc::Document::flagAsDirty() { mDirty = true; } openmw-openmw-0.49.0/apps/opencs/model/doc/document.hpp000066400000000000000000000110331503074453300230440ustar00rootroot00000000000000#ifndef CSM_DOC_DOCUMENT_H #define CSM_DOC_DOCUMENT_H #include #include #include #include #include #include #include #include #include #include "../world/data.hpp" #include "../world/idcompletionmanager.hpp" #include "../tools/tools.hpp" #include "operationholder.hpp" #include "runner.hpp" #include "saving.hpp" class QTextDocument; namespace CSMTools { class ReportModel; class Search; } namespace ESM { struct GameSetting; struct Global; struct MagicEffect; } namespace Files { struct ConfigurationManager; } namespace CSMDoc { struct Message; class Document : public QObject { Q_OBJECT 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; Runner mRunner; bool mDirty; CSMWorld::IdCompletionManager mIdCompletionManager; // 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; // not implemented Document(const Document&); Document& operator=(const Document&); void createBase(); void addGmsts(); void addOptionalGmsts(); void addOptionalGlobals(); void addOptionalMagicEffects(); void addOptionalGmst(const ESM::GameSetting& gmst); void addOptionalGlobal(const ESM::Global& global); void addOptionalMagicEffect(const ESM::MagicEffect& effect); 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 Files::PathContainer& dataPaths, const std::vector& archives); ~Document() override = default; QUndoStack& getUndoStack(); int getState() const; const std::filesystem::path& getResourceDir() const; const std::filesystem::path& getSavePath() 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. bool isNew() const; ///< Is this a newly created content file? void save(); CSMWorld::UniversalId verify(const CSMWorld::UniversalId& reportId = CSMWorld::UniversalId()); CSMWorld::UniversalId newSearch(); void runSearch(const CSMWorld::UniversalId& searchId, const CSMTools::Search& search); void runMerge(std::unique_ptr target); void abortOperation(int type); const CSMWorld::Data& getData() const; CSMWorld::Data& getData(); CSMTools::ReportModel* getReport(const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. void startRunning(const std::string& profile, const std::string& startupInstruction = ""); void stopRunning(); QTextDocument* getRunLog(); CSMWorld::IdCompletionManager& getIdCompletionManager(); void flagAsDirty(); signals: void stateChanged(int state, 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); void operationDone(int type, bool failed); private slots: void modificationStateChanged(bool clean); void reportMessage(const CSMDoc::Message& message, int type); void operationDone2(int type, bool failed); void runStateChanged(); public slots: void progress(int current, int max, int type); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/documentmanager.cpp000066400000000000000000000072601503074453300244010ustar00rootroot00000000000000#include "documentmanager.hpp" #include #include #include #include #include #ifndef Q_MOC_RUN #include #endif #include "document.hpp" CSMDoc::DocumentManager::DocumentManager(const Files::ConfigurationManager& configuration) : mConfiguration(configuration) , mEncoding(ToUTF8::WINDOWS_1252) { std::filesystem::path projectPath = configuration.getUserDataPath() / "projects"; if (!std::filesystem::is_directory(projectPath)) std::filesystem::create_directories(projectPath); mLoader.moveToThread(&mLoaderThread); mLoaderThread.start(); 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() { mLoaderThread.quit(); mLoader.stop(); mLoader.hasThingsToDo().wakeAll(); mLoaderThread.wait(); for (std::vector::iterator iter(mDocuments.begin()); iter != mDocuments.end(); ++iter) delete *iter; } bool CSMDoc::DocumentManager::isEmpty() { return mDocuments.empty(); } void CSMDoc::DocumentManager::addDocument( const std::vector& files, const std::filesystem::path& savePath, bool new_) { Document* document = makeDocument(files, savePath, new_); insertDocument(document); } 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, mDataPaths, mArchives); } void CSMDoc::DocumentManager::insertDocument(CSMDoc::Document* document) { mDocuments.push_back(document); connect(document, SIGNAL(mergeDone(CSMDoc::Document*)), this, SLOT(insertDocument(CSMDoc::Document*))); emit loadRequest(document); mLoader.hasThingsToDo().wakeAll(); } void CSMDoc::DocumentManager::removeDocument(CSMDoc::Document* document) { std::vector::iterator iter = std::find(mDocuments.begin(), mDocuments.end(), document); if (iter == mDocuments.end()) throw std::runtime_error("removing invalid document"); emit documentAboutToBeRemoved(document); mDocuments.erase(iter); document->deleteLater(); if (mDocuments.empty()) emit lastDocumentDeleted(); } void CSMDoc::DocumentManager::setResourceDir(const std::filesystem::path& parResDir) { mResDir = std::filesystem::absolute(parResDir); } void CSMDoc::DocumentManager::setEncoding(ToUTF8::FromType encoding) { mEncoding = encoding; } void CSMDoc::DocumentManager::documentLoaded(Document* document) { emit documentAdded(document); emit loadingStopped(document, true, ""); } void CSMDoc::DocumentManager::documentNotLoaded(Document* document, const std::string& error) { emit loadingStopped(document, false, error); if (error.empty()) // do not remove the document yet, if we have an error removeDocument(document); } void CSMDoc::DocumentManager::setFileData( const Files::PathContainer& dataPaths, const std::vector& archives) { mDataPaths = dataPaths; mArchives = archives; } openmw-openmw-0.49.0/apps/opencs/model/doc/documentmanager.hpp000066400000000000000000000066601503074453300244110ustar00rootroot00000000000000#ifndef CSM_DOC_DOCUMENTMGR_H #define CSM_DOC_DOCUMENTMGR_H #include #include #include #include #include #include #include #include "loader.hpp" namespace Files { struct ConfigurationManager; } namespace CSMDoc { class Document; class DocumentManager : public QObject { Q_OBJECT std::vector mDocuments; const Files::ConfigurationManager& mConfiguration; QThread mLoaderThread; Loader mLoader; ToUTF8::FromType mEncoding; std::filesystem::path mResDir; Files::PathContainer mDataPaths; std::vector mArchives; DocumentManager(const DocumentManager&); DocumentManager& operator=(const DocumentManager&); public: DocumentManager(const Files::ConfigurationManager& configuration); ~DocumentManager(); 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& files, const std::filesystem::path& savePath, bool new_); void setResourceDir(const std::filesystem::path& parResDir); void setEncoding(ToUTF8::FromType encoding); /// Sets the file data that gets passed to newly created documents. void setFileData(const Files::PathContainer& dataPaths, const std::vector& archives); bool isEmpty(); private slots: 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. public slots: 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); signals: void documentAdded(CSMDoc::Document* document); void documentAboutToBeRemoved(CSMDoc::Document* document); void loadRequest(CSMDoc::Document* document); void lastDocumentDeleted(); void loadingStopped(CSMDoc::Document* document, bool completed, const std::string& error); void nextStage(CSMDoc::Document* document, const std::string& name, int totalRecords); void nextRecord(CSMDoc::Document* document, int records); void cancelLoading(CSMDoc::Document* document); void loadMessage(CSMDoc::Document* document, const std::string& message); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/loader.cpp000066400000000000000000000105731503074453300224770ustar00rootroot00000000000000#include "loader.hpp" #include #include #include #include #include #include #include #include #include #include "../tools/reportmodel.hpp" #include "document.hpp" CSMDoc::Loader::Loader() : mShouldStop(false) { mTimer = new QTimer(this); connect(mTimer, &QTimer::timeout, this, &Loader::load); mTimer->start(); } QWaitCondition& CSMDoc::Loader::hasThingsToDo() { return mThingsToDo; } void CSMDoc::Loader::stop() { mShouldStop = true; } void CSMDoc::Loader::load() { if (mDocuments.empty()) { mMutex.lock(); mThingsToDo.wait(&mMutex); mMutex.unlock(); if (mShouldStop) mTimer->stop(); return; } if (!mStart.has_value()) mStart = std::chrono::steady_clock::now(); std::vector>::iterator iter = mDocuments.begin(); Document* document = iter->first; int size = static_cast(document->getContentFiles().size()); int editedIndex = size - 1; // index of the file to be edited/created if (document->isNew()) --size; bool done = false; try { if (iter->second.mRecordsLeft) { Messages messages(Message::Severity_Error); const int batchingSize = 50; 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; } else ++(iter->second.mRecordsLoaded); 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); } } emit nextRecord(document, iter->second.mRecordsLoaded); return; } if (iter->second.mFile < size) // start loading the files { const std::filesystem::path& path = document->getContentFiles()[iter->second.mFile]; int steps = document->getData().startLoading(path, iter->second.mFile != editedIndex, /*project*/ false); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage(document, Files::pathToUnicodeString(path.filename()), steps); } else if (iter->second.mFile == size) // start loading the last (project) file { int steps = document->getData().startLoading(document->getProjectPath(), /*base*/ false, true); iter->second.mRecordsLeft = true; iter->second.mRecordsLoaded = 0; emit nextStage(document, "Project File", steps); } else { document->getData().finishLoading(); done = true; } ++(iter->second.mFile); } catch (const std::exception& e) { mDocuments.erase(iter); emit documentNotLoaded(document, e.what()); return; } if (done) { 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) { mDocuments.emplace_back(document, Stage()); } void CSMDoc::Loader::abortLoading(CSMDoc::Document* document) { for (std::vector>::iterator iter = mDocuments.begin(); iter != mDocuments.end(); ++iter) { if (iter->first == document) { mDocuments.erase(iter); emit documentNotLoaded(document, ""); break; } } } openmw-openmw-0.49.0/apps/opencs/model/doc/loader.hpp000066400000000000000000000041601503074453300224770ustar00rootroot00000000000000#ifndef CSM_DOC_LOADER_H #define CSM_DOC_LOADER_H #include #include #include #include #include #include #include #include class QTimer; namespace CSMDoc { class Document; class Loader : public QObject { Q_OBJECT struct Stage { int mFile = 0; int mRecordsLoaded = 0; bool mRecordsLeft = false; }; QMutex mMutex; QWaitCondition mThingsToDo; std::vector> mDocuments; QTimer* mTimer; bool mShouldStop; std::optional mStart; public: Loader(); QWaitCondition& hasThingsToDo(); void stop(); private slots: void load(); public slots: 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. signals: 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 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 loadMessage(CSMDoc::Document* document, const std::string& message); ///< Non-critical load error or warning }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/messages.cpp000066400000000000000000000026171503074453300230400ustar00rootroot00000000000000#include "messages.hpp" #include 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) { 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; } return ""; } 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) { if (severity == Message::Severity_Default) severity = mDefault; mMessages.push_back(Message(id, message, hint, severity)); } CSMDoc::Messages::Iterator CSMDoc::Messages::begin() const { return mMessages.begin(); } CSMDoc::Messages::Iterator CSMDoc::Messages::end() const { return mMessages.end(); } openmw-openmw-0.49.0/apps/opencs/model/doc/messages.hpp000066400000000000000000000030211503074453300230330ustar00rootroot00000000000000#ifndef CSM_DOC_MESSAGES_H #define CSM_DOC_MESSAGES_H #include #include #include #include #include "../world/universalid.hpp" namespace CSMDoc { struct Message { 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_SeriousError = 3, // an error so bad we can't even be sure if we are // reporting it correctly Severity_Default = 4 }; CSMWorld::UniversalId mId; std::string mMessage; std::string mHint; Severity mSeverity; Message(); Message( const CSMWorld::UniversalId& id, const std::string& message, const std::string& hint, Severity severity); static std::string toString(Severity severity); }; class Messages { public: typedef std::vector Collection; typedef Collection::const_iterator Iterator; private: Collection mMessages; Message::Severity mDefault; 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); Iterator begin() const; Iterator end() const; }; } Q_DECLARE_METATYPE(CSMDoc::Message) #endif openmw-openmw-0.49.0/apps/opencs/model/doc/operation.cpp000066400000000000000000000104051503074453300232230ustar00rootroot00000000000000#include "operation.hpp" #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(); mCurrentStep = 0; mCurrentStepTotal = 0; mTotalSteps = 0; mError = false; for (std::vector>::iterator iter(mStages.begin()); iter != mStages.end(); ++iter) { iter->second = iter->first->setup(); mTotalSteps += iter->second; } } 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); } CSMDoc::Operation::~Operation() { for (std::vector>::iterator iter(mStages.begin()); iter != mStages.end(); ++iter) delete iter->first; } void CSMDoc::Operation::run() { mTimer->stop(); if (!mConnected) { connect(mTimer, &QTimer::timeout, this, &Operation::executeStage); mConnected = true; } mPrepared = false; mStart = std::chrono::steady_clock::now(); mTimer->start(0); } void CSMDoc::Operation::appendStage(Stage* stage) { mStages.emplace_back(stage, 0); } void CSMDoc::Operation::setDefaultSeverity(Message::Severity severity) { mDefaultSeverity = severity; } bool CSMDoc::Operation::hasError() const { return mError; } void CSMDoc::Operation::abort() { if (!mTimer->isActive()) return; mError = true; if (mFinalAlways) { if (mStages.begin() != mStages.end() && mCurrentStage != --mStages.end()) { mCurrentStep = 0; mCurrentStage = --mStages.end(); } } else mCurrentStage = mStages.end(); } void CSMDoc::Operation::executeStage() { if (!mPrepared) { prepareStages(); mPrepared = true; } Messages messages(mDefaultSeverity); while (mCurrentStage != mStages.end()) { if (mCurrentStep >= mCurrentStage->second) { mCurrentStep = 0; ++mCurrentStage; } else { try { mCurrentStage->first->perform(mCurrentStep++, messages); } catch (const std::exception& e) { emit reportMessage( Message(CSMWorld::UniversalId(), e.what(), "", Message::Severity_SeriousError), mType); abort(); } ++mCurrentStepTotal; break; } } emit progress(mCurrentStepTotal, mTotalSteps ? mTotalSteps : 1, 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(); } operationDone(); } } void CSMDoc::Operation::operationDone() { mTimer->stop(); emit done(mType, mError); } openmw-openmw-0.49.0/apps/opencs/model/doc/operation.hpp000066400000000000000000000035341503074453300232350ustar00rootroot00000000000000#ifndef CSM_DOC_OPERATION_H #define CSM_DOC_OPERATION_H #include #include #include #include #include #include "messages.hpp" #include "state.hpp" class QTimer; namespace CSMDoc { class Stage; class Operation : public QObject { Q_OBJECT 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; void prepareStages(); 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(); 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); bool hasError() const; signals: void progress(int current, int max, int type); void reportMessage(const CSMDoc::Message& message, int type); void done(int type, bool failed); public slots: void abort(); void run(); private slots: void executeStage(); protected slots: virtual void operationDone(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/operationholder.cpp000066400000000000000000000024031503074453300244200ustar00rootroot00000000000000#include "operationholder.hpp" #include "operation.hpp" CSMDoc::OperationHolder::OperationHolder(Operation* operation) : mOperation(nullptr) , mRunning(false) { if (operation) setOperation(operation); } void CSMDoc::OperationHolder::setOperation(Operation* operation) { mOperation = operation; mOperation->moveToThread(&mThread); connect(mOperation, &Operation::progress, this, &OperationHolder::progress); connect(mOperation, &Operation::reportMessage, this, &OperationHolder::reportMessage); connect(mOperation, &Operation::done, this, &OperationHolder::doneSlot); connect(this, &OperationHolder::abortSignal, mOperation, &Operation::abort); connect(&mThread, &QThread::started, mOperation, &Operation::run); } bool CSMDoc::OperationHolder::isRunning() const { return mRunning; } void CSMDoc::OperationHolder::start() { mRunning = true; mThread.start(); } void CSMDoc::OperationHolder::abort() { mRunning = false; emit abortSignal(); } void CSMDoc::OperationHolder::abortAndWait() { if (mRunning) { mThread.quit(); mThread.wait(); } } void CSMDoc::OperationHolder::doneSlot(int type, bool failed) { mRunning = false; mThread.quit(); emit done(type, failed); } openmw-openmw-0.49.0/apps/opencs/model/doc/operationholder.hpp000066400000000000000000000015761503074453300244370ustar00rootroot00000000000000#ifndef CSM_DOC_OPERATIONHOLDER_H #define CSM_DOC_OPERATIONHOLDER_H #include #include namespace CSMDoc { class Operation; struct Message; class OperationHolder : public QObject { Q_OBJECT QThread mThread; Operation* mOperation; bool mRunning; public: OperationHolder(Operation* operation = nullptr); void setOperation(Operation* operation); bool isRunning() const; void start(); void abort(); // Abort and wait until thread has finished. void abortAndWait(); private slots: void doneSlot(int type, bool failed); signals: void progress(int current, int max, int type); void reportMessage(const CSMDoc::Message& message, int type); void done(int type, bool failed); void abortSignal(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/runner.cpp000066400000000000000000000074541503074453300225460ustar00rootroot00000000000000#include "runner.hpp" #include #include #include #include #include #include #include #include #include #include "operationholder.hpp" CSMDoc::Runner::Runner(std::filesystem::path projectPath) : mRunning(false) , mStartup(nullptr) , mProjectPath(std::move(projectPath)) { connect(&mProcess, qOverload(&QProcess::finished), this, &Runner::finished); connect(&mProcess, &QProcess::readyReadStandardOutput, this, &Runner::readyReadStandardOutput); mProcess.setProcessChannelMode(QProcess::MergedChannels); mProfile.blank(); } CSMDoc::Runner::~Runner() { if (mRunning) { disconnect(&mProcess, nullptr, this, nullptr); mProcess.kill(); mProcess.waitForFinished(); } } void CSMDoc::Runner::start(bool delayed) { if (mStartup) { delete mStartup; mStartup = nullptr; } if (!delayed) { mLog.clear(); QString path = "openmw"; #ifdef Q_OS_WIN path.append(QLatin1String(".exe")); #endif QDir dir(QCoreApplication::applicationDirPath()); #ifdef Q_OS_MAC // the CS and engine are in separate .app directories dir.cdUp(); dir.cdUp(); dir.cdUp(); path.prepend("OpenMW.app/Contents/MacOS/"); #endif path = dir.absoluteFilePath(path); mStartup = new QTemporaryFile(this); mStartup->open(); { QTextStream stream(mStartup); if (!mStartupInstruction.empty()) stream << QString::fromUtf8(mStartupInstruction.c_str()) << '\n'; stream << QString::fromUtf8(mProfile.mScriptText.c_str()); } mStartup->close(); QStringList arguments; arguments << "--skip-menu"; if (mProfile.mFlags & ESM::DebugProfile::Flag_BypassNewGame) arguments << "--new-game=0"; else arguments << "--new-game=1"; arguments << ("--script-run=" + mStartup->fileName()); arguments << "--data=\"" + Files::pathToQString(mProjectPath.parent_path()) + "\""; arguments << "--replace=content"; for (const auto& mContentFile : mContentFiles) { arguments << "--content=" + Files::pathToQString(mContentFile); } arguments << "--content=" + Files::pathToQString(mProjectPath.filename()); mProcess.start(path, arguments); } mRunning = true; emit runStateChanged(); } void CSMDoc::Runner::stop() { delete mStartup; mStartup = nullptr; if (mProcess.state() == QProcess::NotRunning) { mRunning = false; emit runStateChanged(); } else mProcess.kill(); } bool CSMDoc::Runner::isRunning() const { return mRunning; } 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) { mRunning = false; emit runStateChanged(); } QTextDocument* CSMDoc::Runner::getLog() { return &mLog; } void CSMDoc::Runner::readyReadStandardOutput() { mLog.setPlainText(mLog.toPlainText() + QString::fromUtf8(mProcess.readAllStandardOutput())); } CSMDoc::SaveWatcher::SaveWatcher(Runner* runner, OperationHolder* operation) : QObject(runner) , mRunner(runner) { connect(operation, &OperationHolder::done, this, &SaveWatcher::saveDone); } void CSMDoc::SaveWatcher::saveDone(int type, bool failed) { if (failed) mRunner->stop(); else mRunner->start(); deleteLater(); } openmw-openmw-0.49.0/apps/opencs/model/doc/runner.hpp000066400000000000000000000035731503074453300225510ustar00rootroot00000000000000#ifndef CSM_DOC_RUNNER_H #define CSM_DOC_RUNNER_H #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; std::filesystem::path mProjectPath; public: Runner(std::filesystem::path projectPath); ~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); 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; void configure(const ESM::DebugProfile& profile, const std::vector& contentFiles, const std::string& startupInstruction); QTextDocument* getLog(); signals: void runStateChanged(); private slots: void finished(int exitCode, QProcess::ExitStatus exitStatus); void readyReadStandardOutput(); }; /// \brief Watch for end of save operation and restart or stop runner class SaveWatcher : public QObject { Q_OBJECT Runner* mRunner; public: /// *this attaches itself to runner SaveWatcher(Runner* runner, OperationHolder* operation); private slots: void saveDone(int type, bool failed); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/saving.cpp000066400000000000000000000124461503074453300225210ustar00rootroot00000000000000#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 "document.hpp" #include "savingstages.hpp" #include "state.hpp" 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 WriteHeaderStage(mDocument, mState, true)); appendStage(new WriteCollectionStage>( mDocument.getData().getFilters(), mState, CSMWorld::Scope_Project)); appendStage(new WriteCollectionStage>( mDocument.getData().getDebugProfiles(), mState, CSMWorld::Scope_Project)); appendStage(new WriteCollectionStage>( mDocument.getData().getScripts(), mState, CSMWorld::Scope_Project)); 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 WriteHeaderStage(mDocument, mState, false)); appendStage( new WriteCollectionStage>(mDocument.getData().getGlobals(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getGmsts(), mState)); appendStage(new WriteCollectionStage>(mDocument.getData().getSkills(), mState)); appendStage(new WriteCollectionStage>(mDocument.getData().getClasses(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getFactions(), mState)); appendStage(new WriteCollectionStage>(mDocument.getData().getRaces(), mState)); appendStage(new WriteCollectionStage>(mDocument.getData().getSounds(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getScripts(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getRegions(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getBirthsigns(), mState)); appendStage(new WriteCollectionStage>(mDocument.getData().getSpells(), mState)); appendStage(new WriteCollectionStage>( mDocument.getData().getEnchantments(), mState)); appendStage( new WriteCollectionStage>(mDocument.getData().getBodyParts(), mState)); appendStage(new WriteCollectionStage>( mDocument.getData().getMagicEffects(), mState)); appendStage(new WriteCollectionStage>( mDocument.getData().getStartScripts(), mState)); appendStage(new WriteRefIdCollectionStage(mDocument, mState)); // Can reference creatures so needs to load after them for TESCS compatibility appendStage(new WriteCollectionStage>( mDocument.getData().getSoundGens(), mState)); appendStage(new CollectionReferencesStage(mDocument, mState)); appendStage(new WriteCellCollectionStage(mDocument, mState)); // Dialogue can reference objects, cells, and journals so must be written after these records for vanilla-compatible // files appendStage(new WriteDialogueCollectionStage(mDocument, mState, true)); appendStage(new WriteDialogueCollectionStage(mDocument, mState, false)); appendStage(new WritePathgridCollectionStage(mDocument, mState)); appendStage(new WriteLandTextureCollectionStage(mDocument, mState)); // references Land Textures appendStage(new WriteLandCollectionStage(mDocument, mState)); // close file and clean up appendStage(new CloseSaveStage(mState)); appendStage(new FinalSavingStage(mDocument, mState)); } openmw-openmw-0.49.0/apps/opencs/model/doc/saving.hpp000066400000000000000000000007421503074453300225220ustar00rootroot00000000000000#ifndef CSM_DOC_SAVING_H #define CSM_DOC_SAVING_H #include #include #include "operation.hpp" #include "savingstate.hpp" #include namespace CSMDoc { class Document; class Saving : public Operation { Q_OBJECT Document& mDocument; SavingState mState; public: Saving(Document& document, const std::filesystem::path& projectPath, ToUTF8::FromType encoding); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/savingstages.cpp000066400000000000000000000455131503074453300237310ustar00rootroot00000000000000#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 "../world/cellcoordinates.hpp" #include "document.hpp" 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) { mState.start(mDocument, mProjectFile); 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"); } 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) { mState.getWriter().setVersion(); mState.getWriter().clearMaster(); if (mSimple) { 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)); /// \todo refine dependency list (at least remove redundant dependencies) std::vector dependencies = mDocument.getContentFiles(); std::vector::const_iterator end(--dependencies.end()); for (std::vector::const_iterator iter(dependencies.begin()); iter != end; ++iter) { auto name = Files::pathToUnicodeString(iter->filename()); auto size = std::filesystem::file_size(*iter); mState.getWriter().addMaster(name, size); } } 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()) { } int CSMDoc::WriteDialogueCollectionStage::setup() { mInfosByTopic = mInfos.getInfosByTopic(); return mTopics.getSize(); } void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages) { ESM::ESMWriter& writer = mState.getWriter(); 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. const ESM::Dialogue& dialogue = topic.get(); writer.startRecord(dialogue.sRecordId); dialogue.save(writer, true); writer.endRecord(dialogue.sRecordId); return; } // Test, if we need to save anything associated info records. bool infoModified = false; const auto topicInfos = mInfosByTopic.find(topic.get().mId); if (topicInfos != mInfosByTopic.end()) { for (const auto& record : topicInfos->second) { 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) { 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); } // write modified selected info records if (topicInfos != mInfosByTopic.end()) { const std::vector*>& infos = topicInfos->second; for (auto iter = infos.begin(); iter != infos.end(); ++iter) { const CSMWorld::Record& record = **iter; if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { ESM::DialInfo info = record.get(); info.mId = record.get().mOriginalId; info.mData.mType = topic.get().mType; if (iter == infos.begin()) info.mPrev = ESM::RefId(); else info.mPrev = (*std::prev(iter))->get().mOriginalId; const auto next = std::next(iter); if (next == infos.end()) info.mNext = ESM::RefId(); else info.mNext = (*next)->get().mOriginalId; 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) { } int CSMDoc::WriteRefIdCollectionStage::setup() { return mDocument.getData().getReferenceables().getSize(); } void CSMDoc::WriteRefIdCollectionStage::perform(int stage, Messages& messages) { mDocument.getData().getReferenceables().save(stage, mState.getWriter()); } CSMDoc::CollectionReferencesStage::CollectionReferencesStage(Document& document, SavingState& state) : mDocument(document) , mState(state) { } int CSMDoc::CollectionReferencesStage::setup() { mState.clearSubRecords(); int size = mDocument.getData().getReferences().getSize(); int steps = size / 100; if (size % 100) ++steps; return steps; } void CSMDoc::CollectionReferencesStage::perform(int stage, Messages& messages) { int size = mDocument.getData().getReferences().getSize(); for (int i = stage * 100; i < stage * 100 + 100 && i < size; ++i) { const CSMWorld::Record& record = mDocument.getData().getReferences().getRecord(i); if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { ESM::RefId cellId = record.get().mOriginalCell.empty() ? record.get().mCell : record.get().mOriginalCell; std::deque& indices = mState.getOrInsertSubRecord(cellId); // collect moved references at the end of the container const bool interior = !cellId.startsWith("#"); std::ostringstream stream; if (!interior) { // recalculate the ref's cell location std::pair index = record.get().getCellIndex(); 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) != cellId && !interior && record.mState != CSMWorld::RecordBase::State_ModifiedOnly && !record.get().mNew) indices.push_back(i); else indices.push_front(i); } } } CSMDoc::WriteCellCollectionStage::WriteCellCollectionStage(Document& document, SavingState& state) : mDocument(document) , mState(state) { } int CSMDoc::WriteCellCollectionStage::setup() { return mDocument.getData().getCells().getSize(); } 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::RefIdCollection& referenceables = mDocument.getData().getReferenceables(); const CSMWorld::RefIdData& refIdData = referenceables.getDataSet(); std::deque tempRefs; std::deque persistentRefs; const std::deque* references = mState.findSubRecord(cell.get().mId); if (cell.isModified() || cell.mState == CSMWorld::RecordBase::State_Deleted || references != nullptr) { CSMWorld::Cell cellRecord = cell.get(); const bool interior = !cellRecord.mId.startsWith("#"); // count new references and adjust RefNumCount accordingsly unsigned int newRefNum = cellRecord.mRefNumCounter; if (references != nullptr) { for (std::deque::const_iterator iter(references->begin()); iter != references->end(); ++iter) { const CSMWorld::Record& ref = mDocument.getData().getReferences().getRecord(*iter); CSMWorld::CellRef refRecord = ref.get(); 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); if (interior) cellRecord.mData.mFlags |= ESM::Cell::Interior; else { cellRecord.mData.mFlags &= ~ESM::Cell::Interior; 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); // write references if (references != nullptr) { writeReferences(persistentRefs, interior, newRefNum); cellRecord.saveTempMarker(writer, static_cast(references->size()) - persistentRefs.size()); writeReferences(tempRefs, interior, newRefNum); } writer.endRecord(cellRecord.sRecordId); } } 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) { ESM::ESMWriter& writer = mState.getWriter(); 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.startsWith("#")) { 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); } } 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) { ESM::ESMWriter& writer = mState.getWriter(); 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); } } 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) { ESM::ESMWriter& writer = mState.getWriter(); const CSMWorld::Record& landTexture = mDocument.getData().getLandTextures().getRecord(stage); if (landTexture.isModified() || landTexture.mState == CSMWorld::RecordBase::State_Deleted) { 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) { } int CSMDoc::CloseSaveStage::setup() { return 1; } void CSMDoc::CloseSaveStage::perform(int stage, Messages& messages) { mState.getStream().close(); if (!mState.getStream()) throw std::runtime_error("saving failed"); } 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) { if (mState.hasError()) { mState.getWriter().close(); mState.getStream().close(); if (std::filesystem::exists(mState.getTmpPath())) std::filesystem::remove(mState.getTmpPath()); } else if (!mState.isProjectFile()) { if (std::filesystem::exists(mState.getPath())) std::filesystem::remove(mState.getPath()); std::filesystem::rename(mState.getTmpPath(), mState.getPath()); mDocument.getUndoStack().setClean(); } } openmw-openmw-0.49.0/apps/opencs/model/doc/savingstages.hpp000066400000000000000000000162261503074453300237350ustar00rootroot00000000000000#ifndef CSM_DOC_SAVINGSTAGES_H #define CSM_DOC_SAVINGSTAGES_H #include "stage.hpp" #include #include #include "../world/idcollection.hpp" #include "../world/infocollection.hpp" #include "../world/record.hpp" #include "../world/scope.hpp" #include "savingstate.hpp" namespace ESM { struct Dialogue; class ESMWriter; } namespace CSMWorld { class InfoCollection; } namespace CSMDoc { class Document; class Messages; class OpenSaveStage : public Stage { Document& mDocument; SavingState& mState; bool mProjectFile; 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 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: WriteHeaderStage(Document& document, SavingState& state, bool simple); ///< \param simple Simplified header (used for project files). 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. }; template class WriteCollectionStage : public Stage { const CollectionT& mCollection; SavingState& mState; CSMWorld::Scope mScope; public: WriteCollectionStage( const CollectionT& collection, SavingState& state, CSMWorld::Scope scope = CSMWorld::Scope_Content); 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. }; template WriteCollectionStage::WriteCollectionStage( const CollectionT& collection, SavingState& state, CSMWorld::Scope scope) : mCollection(collection) , mState(state) , mScope(scope) { } template int WriteCollectionStage::setup() { return mCollection.getSize(); } template void WriteCollectionStage::perform(int stage, Messages& messages) { 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(); if (state == CSMWorld::RecordBase::State_Modified || state == CSMWorld::RecordBase::State_ModifiedOnly || state == CSMWorld::RecordBase::State_Deleted) { 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; CSMWorld::InfosRecordPtrByTopic mInfosByTopic; public: WriteDialogueCollectionStage(Document& document, SavingState& state, bool journal); 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. }; class WriteRefIdCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteRefIdCollectionStage(Document& document, SavingState& state); 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. }; class CollectionReferencesStage : public Stage { Document& mDocument; SavingState& mState; public: CollectionReferencesStage(Document& document, SavingState& state); 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. }; class WriteCellCollectionStage : public Stage { Document& mDocument; SavingState& mState; void writeReferences(const std::deque& references, bool interior, unsigned int& newRefNum); public: WriteCellCollectionStage(Document& document, SavingState& state); 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. }; class WritePathgridCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WritePathgridCollectionStage(Document& document, SavingState& state); 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. }; class WriteLandCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteLandCollectionStage(Document& document, SavingState& state); 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. }; class WriteLandTextureCollectionStage : public Stage { Document& mDocument; SavingState& mState; public: WriteLandTextureCollectionStage(Document& document, SavingState& state); 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. }; class CloseSaveStage : public Stage { SavingState& mState; public: CloseSaveStage(SavingState& state); 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. }; class FinalSavingStage : public Stage { Document& mDocument; SavingState& mState; public: FinalSavingStage(Document& document, SavingState& state); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/savingstate.cpp000066400000000000000000000033051503074453300235540ustar00rootroot00000000000000#include "savingstate.hpp" #include #include #include "document.hpp" #include "operation.hpp" 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); } bool CSMDoc::SavingState::hasError() const { return mOperation.hasError(); } void CSMDoc::SavingState::start(Document& document, bool project) { mProjectFile = project; if (mStream.is_open()) mStream.close(); mStream.clear(); mSubRecords.clear(); if (project) mPath = mProjectPath; else mPath = document.getSavePath(); std::filesystem::path file(mPath.filename().u8string() + u8".tmp"); mTmpPath = mPath.parent_path(); mTmpPath /= file; } const std::filesystem::path& CSMDoc::SavingState::getPath() const { return mPath; } const std::filesystem::path& CSMDoc::SavingState::getTmpPath() const { return mTmpPath; } std::ofstream& CSMDoc::SavingState::getStream() { return mStream; } ESM::ESMWriter& CSMDoc::SavingState::getWriter() { return mWriter; } bool CSMDoc::SavingState::isProjectFile() const { return mProjectFile; } 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[refId]; } void CSMDoc::SavingState::clearSubRecords() { mSubRecords.clear(); } openmw-openmw-0.49.0/apps/opencs/model/doc/savingstate.hpp000066400000000000000000000027501503074453300235640ustar00rootroot00000000000000#ifndef CSM_DOC_SAVINGSTATE_H #define CSM_DOC_SAVINGSTATE_H #include #include #include #include #include #include #include #include namespace CSMDoc { class Operation; class Document; class SavingState { 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: SavingState(Operation& operation, std::filesystem::path projectPath, ToUTF8::FromType encoding); bool hasError() const; void start(Document& document, bool project); ///< \param project Save project file instead of content file. const std::filesystem::path& getPath() const; const std::filesystem::path& getTmpPath() const; std::ofstream& getStream(); ESM::ESMWriter& getWriter(); bool isProjectFile() const; ///< Currently saving project file? (instead of content file) const std::deque* findSubRecord(const ESM::RefId& refId) const; std::deque& getOrInsertSubRecord(const ESM::RefId& refId); void clearSubRecords(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/stage.hpp000066400000000000000000000006111503074453300223310ustar00rootroot00000000000000#ifndef CSM_DOC_STAGE_H #define CSM_DOC_STAGE_H namespace CSMDoc { class Messages; class Stage { public: virtual ~Stage() = default; 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/doc/state.hpp000066400000000000000000000006461503074453300223560ustar00rootroot00000000000000#ifndef CSM_DOC_STATE_H #define CSM_DOC_STATE_H namespace CSMDoc { enum State { State_Modified = 1, State_Locked = 2, State_Operation = 4, State_Running = 8, State_Saving = 16, State_Verifying = 32, State_Merging = 64, State_Searching = 128, State_Loading = 256 // pseudo-state; can not be encountered in a loaded document }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/000077500000000000000000000000001503074453300212375ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/filter/andnode.cpp000066400000000000000000000007731503074453300233620ustar00rootroot00000000000000#include "andnode.hpp" #include #include 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 { int size = getSize(); for (int i = 0; i < size; ++i) if (!(*this)[i].test(table, row, columns)) return false; return true; } openmw-openmw-0.49.0/apps/opencs/model/filter/andnode.hpp000066400000000000000000000011371503074453300233620ustar00rootroot00000000000000#ifndef CSM_FILTER_ANDNODE_H #define CSM_FILTER_ANDNODE_H #include "narynode.hpp" #include #include #include namespace CSMWorld { class IdTableBase; } namespace CSMFilter { class Node; class AndNode : public NAryNode { 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 }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/booleannode.cpp000066400000000000000000000006231503074453300242310ustar00rootroot00000000000000#include "booleannode.hpp" 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 { return mTrue; } std::string CSMFilter::BooleanNode::toString(bool numericColumns) const { return mTrue ? "true" : "false"; } openmw-openmw-0.49.0/apps/opencs/model/filter/booleannode.hpp000066400000000000000000000014361503074453300242410ustar00rootroot00000000000000#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 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/leafnode.cpp000066400000000000000000000001771503074453300235250ustar00rootroot00000000000000#include "leafnode.hpp" std::vector CSMFilter::LeafNode::getReferencedColumns() const { return std::vector(); } openmw-openmw-0.49.0/apps/opencs/model/filter/leafnode.hpp000066400000000000000000000006561503074453300235340ustar00rootroot00000000000000#ifndef CSM_FILTER_LEAFNODE_H #define CSM_FILTER_LEAFNODE_H #include #include "node.hpp" 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/narynode.cpp000066400000000000000000000023641503074453300235670ustar00rootroot00000000000000#include "narynode.hpp" #include #include #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 { 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) { std::vector columns2 = (*iter)->getReferencedColumns(); columns.insert(columns.end(), columns2.begin(), columns2.end()); } return columns; } std::string CSMFilter::NAryNode::toString(bool numericColumns) const { std::ostringstream stream; stream << mName << " ("; bool first = true; int size = getSize(); for (int i = 0; i < size; ++i) { if (first) first = false; else stream << ", "; stream << (*this)[i].toString(numericColumns); } stream << ")"; return stream.str(); } openmw-openmw-0.49.0/apps/opencs/model/filter/narynode.hpp000066400000000000000000000016571503074453300236000ustar00rootroot00000000000000#ifndef CSM_FILTER_NARYNODE_H #define CSM_FILTER_NARYNODE_H #include #include #include #include "node.hpp" namespace CSMFilter { class NAryNode : public Node { std::vector> mNodes; std::string mName; public: NAryNode(const std::vector>& nodes, const std::string& name); int getSize() 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::string toString(bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/node.hpp000066400000000000000000000025731503074453300227040ustar00rootroot00000000000000#ifndef CSM_FILTER_NODE_H #define CSM_FILTER_NODE_H #include #include #include #include #include namespace CSMWorld { class IdTableBase; } namespace CSMFilter { /// \brief Root class for the filter node hierarchy /// /// \note When the function documentation for this class mentions "this node", this should be /// interpreted as "the node and all its children". class Node { 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) #endif openmw-openmw-0.49.0/apps/opencs/model/filter/notnode.cpp000066400000000000000000000006701503074453300234140ustar00rootroot00000000000000#include "notnode.hpp" #include #include namespace CSMWorld { 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); } openmw-openmw-0.49.0/apps/opencs/model/filter/notnode.hpp000066400000000000000000000010731503074453300234170ustar00rootroot00000000000000#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); 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 }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/ornode.cpp000066400000000000000000000010441503074453300232300ustar00rootroot00000000000000#include "ornode.hpp" #include #include 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 { int size = getSize(); for (int i = 0; i < size; ++i) if ((*this)[i].test(table, row, columns)) return true; return false; } openmw-openmw-0.49.0/apps/opencs/model/filter/ornode.hpp000066400000000000000000000011331503074453300232340ustar00rootroot00000000000000#ifndef CSM_FILTER_ORNODE_H #define CSM_FILTER_ORNODE_H #include "narynode.hpp" #include #include #include namespace CSMWorld { class IdTableBase; } namespace CSMFilter { class Node; class OrNode : public NAryNode { 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 }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/parser.cpp000066400000000000000000000352411503074453300232440ustar00rootroot00000000000000#include "parser.hpp" #include #include #include #include #include #include #include #include #include #include "../world/columns.hpp" #include "../world/data.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 { enum Type { Type_EOS, Type_None, Type_String, Type_Number, Type_Open, Type_Close, Type_OpenSquare, Type_CloseSquare, Type_Comma, Type_OneShot, Type_Keyword_True, ///< \attention Keyword enums must be arranged continuously. Type_Keyword_False, Type_Keyword_And, Type_Keyword_Or, Type_Keyword_Not, Type_Keyword_Text, Type_Keyword_Value }; Type mType; std::string mString; double mNumber; Token(Type type = Type_None); Token(Type type, const std::string& string); ///< Non-string type that can also be interpreted as a string. Token(const std::string& string); Token(double number); operator bool() const; bool isString() const; }; 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(const std::string& string) : mType(Type_String) , mString(string) , mNumber(0.0) { } Token::Token(double number) : mType(Type_Number) , mNumber(number) { } bool Token::isString() const { return mType == Type_String || mType >= Type_Keyword_True; } Token::operator bool() const { return mType != Type_None; } bool operator==(const Token& left, const Token& right) { 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; default: return true; } } } CSMFilter::Token CSMFilter::Parser::getStringToken() { std::string string; int size = static_cast(mInput.size()); for (; mIndex < size; ++mIndex) { char c = mInput[mIndex]; if (isAlpha(c) || c == ':' || c == '_' || (!string.empty() && isDigit(c)) || c == '"' || (!string.empty() && string[0] == '"')) string += c; else break; if (c == '"' && string.size() > 1) { ++mIndex; break; } }; if (!string.empty()) { if (string[0] == '"' && (string[string.size() - 1] != '"' || string.size() < 2)) { error(); return Token(Token::Type_None); } if (string[0] != '"' && string[string.size() - 1] == '"') { error(); return Token(Token::Type_None); } if (string[0] == '"') return string.substr(1, string.size() - 2); } return checkKeywords(string); } CSMFilter::Token CSMFilter::Parser::getNumberToken() { std::string string; int size = static_cast(mInput.size()); bool hasDecimalPoint = false; bool hasDigit = false; for (; mIndex < size; ++mIndex) { char c = mInput[mIndex]; if (isDigit(c)) { string += c; hasDigit = true; } else if (c == '.' && !hasDecimalPoint) { string += c; hasDecimalPoint = true; } else if (string.empty() && c == '-') string += c; else break; } if (!hasDigit) { error(); return Token(Token::Type_None); } float value; std::istringstream stream(string.c_str()); stream >> value; return value; } CSMFilter::Token CSMFilter::Parser::checkKeywords(const Token& token) { static const char* sKeywords[] = { "true", "false", "and", "or", "not", "string", "value", nullptr, }; 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); return token; } CSMFilter::Token CSMFilter::Parser::getNextToken() { int size = static_cast(mInput.size()); char c = 0; for (; mIndex < size; ++mIndex) { c = mInput[mIndex]; if (c != ' ') break; } 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); } if (c == '"' || c == '_' || isAlpha(c) || c == ':') return getStringToken(); if (c == '-' || c == '.' || isDigit(c)) return getNumberToken(); error(); return Token(Token::Type_None); } std::shared_ptr CSMFilter::Parser::parseImp(bool allowEmpty, bool ignoreOneShot) { if (Token token = getNextToken()) { if (token == Token(Token::Type_OneShot)) token = getNextToken(); if (token) switch (token.mType) { case Token::Type_Keyword_True: return std::make_shared(true); case Token::Type_Keyword_False: return std::make_shared(false); case Token::Type_Keyword_And: case Token::Type_Keyword_Or: return parseNAry(token); case Token::Type_Keyword_Not: { std::shared_ptr node = parseImp(); if (mError) return std::shared_ptr(); return std::make_shared(node); } case Token::Type_Keyword_Text: return parseText(); case Token::Type_Keyword_Value: return parseValue(); case Token::Type_EOS: if (!allowEmpty) error(); return std::shared_ptr(); default: error(); } } return std::shared_ptr(); } std::shared_ptr CSMFilter::Parser::parseNAry(const Token& keyword) { std::vector> nodes; Token token = getNextToken(); if (token.mType != Token::Type_Open) { error(); return std::shared_ptr(); } for (;;) { std::shared_ptr node = parseImp(); if (mError) return std::shared_ptr(); nodes.push_back(node); token = getNextToken(); if (!token || (token.mType != Token::Type_Close && token.mType != Token::Type_Comma)) { error(); return std::shared_ptr(); } if (token.mType == Token::Type_Close) break; } switch (keyword.mType) { 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(); } } std::shared_ptr CSMFilter::Parser::parseText() { Token token = getNextToken(); if (token.mType != Token::Type_Open) { error(); return std::shared_ptr(); } token = getNextToken(); if (!token) return std::shared_ptr(); // parse column ID int columnId = -1; if (token.mType == Token::Type_Number) { if (static_cast(token.mNumber) == token.mNumber) columnId = static_cast(token.mNumber); } else if (token.isString()) { columnId = CSMWorld::Columns::getId(token.mString); } if (columnId < 0) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); } // parse text pattern token = getNextToken(); if (!token.isString()) { error(); return std::shared_ptr(); } std::string text = token.mString; token = getNextToken(); if (token.mType != Token::Type_Close) { error(); return std::shared_ptr(); } 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) { error(); return std::shared_ptr(); } token = getNextToken(); if (!token) return std::shared_ptr(); // parse column ID int columnId = -1; if (token.mType == Token::Type_Number) { if (static_cast(token.mNumber) == token.mNumber) columnId = static_cast(token.mNumber); } else if (token.isString()) { columnId = CSMWorld::Columns::getId(token.mString); } if (columnId < 0) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); } // parse value double lower = 0; double upper = 0; ValueNode::Type lowerType = ValueNode::Type_Open; ValueNode::Type upperType = ValueNode::Type_Open; token = getNextToken(); if (token.mType == Token::Type_Number) { // single value lower = upper = token.mNumber; lowerType = upperType = ValueNode::Type_Closed; } else { // interval if (token.mType == Token::Type_OpenSquare) lowerType = ValueNode::Type_Closed; else if (token.mType != Token::Type_CloseSquare && token.mType != Token::Type_Open) { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType == Token::Type_Number) { lower = token.mNumber; token = getNextToken(); if (token.mType != Token::Type_Comma) { error(); return std::shared_ptr(); } } else if (token.mType == Token::Type_Comma) { lowerType = ValueNode::Type_Infinite; } else { error(); return std::shared_ptr(); } token = getNextToken(); if (token.mType == Token::Type_Number) { upper = token.mNumber; token = getNextToken(); } else upperType = ValueNode::Type_Infinite; if (token.mType == Token::Type_CloseSquare) { if (upperType != ValueNode::Type_Infinite) upperType = ValueNode::Type_Closed; } else if (token.mType != Token::Type_OpenSquare && token.mType != Token::Type_Close) { error(); return std::shared_ptr(); } } token = getNextToken(); if (token.mType != Token::Type_Close) { error(); return std::shared_ptr(); } return std::make_shared(columnId, lowerType, upperType, lower, upper); } void CSMFilter::Parser::error() { mError = true; } CSMFilter::Parser::Parser(const CSMWorld::Data& data) : mIndex(0) , mError(false) , mData(data) { } bool CSMFilter::Parser::parse(const std::string& filter, bool allowPredefined) { // reset mFilter.reset(); mError = false; mInput = filter; mIndex = 0; Token token; if (allowPredefined) token = getNextToken(); if (allowPredefined && token == Token(Token::Type_EOS)) { mFilter.reset(); return true; } else if (!allowPredefined || 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)) { error(); return false; } if (node) mFilter = std::move(node); else { // Empty filter string equals to filter "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) { if (getNextToken() != Token(Token::Type_EOS)) { error(); return false; } const int index = mData.getFilters().searchId(ESM::RefId::stringRefId(token.mString)); if (index == -1) { error(); return false; } const CSMWorld::Record& record = mData.getFilters().getRecord(index); if (record.isDeleted()) { error(); return false; } return parse(record.get().mFilter, false); } else { error(); return false; } } std::shared_ptr CSMFilter::Parser::getFilter() const { if (mError) throw std::logic_error("No filter available"); return mFilter; } openmw-openmw-0.49.0/apps/opencs/model/filter/parser.hpp000066400000000000000000000024411503074453300232450ustar00rootroot00000000000000#ifndef CSM_FILTER_PARSER_H #define CSM_FILTER_PARSER_H #include #include namespace CSMFilter { class Node; } namespace CSMWorld { class Data; } namespace CSMFilter { struct Token; class Parser { std::shared_ptr mFilter; std::string mInput; int mIndex; bool mError; const CSMWorld::Data& mData; Token getStringToken(); Token getNumberToken(); Token getNextToken(); Token checkKeywords(const Token& token); ///< Turn string token into keyword token, if possible. 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 parseNAry(const Token& keyword); std::shared_ptr parseText(); std::shared_ptr parseValue(); void error(); public: Parser(const CSMWorld::Data& data); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/textnode.cpp000066400000000000000000000044561503074453300236060ustar00rootroot00000000000000#include "textnode.hpp" #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) , 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 { 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->second == -1) return true; QModelIndex index = table.index(row, iter->second); QVariant data = table.data(index); QString 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))) { int value = data.toInt(); std::vector> enums = CSMWorld::Columns::getEnums(static_cast(mColumnId)); if (value >= 0 && value < static_cast(enums.size())) string = QString::fromUtf8(enums[value].second.c_str()); } else if (data.type() == QVariant::Bool) { string = data.toBool() ? "true" : "false"; } else if (mText.empty() && !data.isValid()) return true; else return false; /// \todo make pattern syntax configurable QRegularExpressionMatch match = mRegExp.match(string); return match.hasMatch(); } std::vector CSMFilter::TextNode::getReferencedColumns() const { return std::vector(1, mColumnId); } std::string CSMFilter::TextNode::toString(bool numericColumns) const { std::ostringstream stream; stream << "text ("; if (numericColumns) stream << mColumnId; else stream << "\"" << CSMWorld::Columns::getName(static_cast(mColumnId)) << "\""; stream << ", \"" << mText << "\")"; return stream.str(); } openmw-openmw-0.49.0/apps/opencs/model/filter/textnode.hpp000066400000000000000000000023131503074453300236010ustar00rootroot00000000000000#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; QRegularExpression mRegExp; public: 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 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. bool isValid() { return mRegExp.isValid(); } }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/unarynode.cpp000066400000000000000000000011571503074453300237530ustar00rootroot00000000000000#include "unarynode.hpp" #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 { return *mChild; } CSMFilter::Node& CSMFilter::UnaryNode::getChild() { return *mChild; } std::vector CSMFilter::UnaryNode::getReferencedColumns() const { return mChild->getReferencedColumns(); } std::string CSMFilter::UnaryNode::toString(bool numericColumns) const { return mName + " " + mChild->toString(numericColumns); } openmw-openmw-0.49.0/apps/opencs/model/filter/unarynode.hpp000066400000000000000000000016041503074453300237550ustar00rootroot00000000000000#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: UnaryNode(std::shared_ptr child, const std::string& name); const Node& getChild() const; 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::string toString(bool numericColumns) const override; ///< Return a string that represents this node. /// /// \param numericColumns Use numeric IDs instead of string to represent columns. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/filter/valuenode.cpp000066400000000000000000000056451503074453300237370ustar00rootroot00000000000000#include "valuenode.hpp" #include #include #include #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) { } bool CSMFilter::ValueNode::test(const CSMWorld::IdTableBase& table, int row, const std::map& columns) const { 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->second == -1) return true; QModelIndex index = table.index(row, iter->second); 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)) return false; double value = data.toDouble(); switch (mLowerType) { case Type_Closed: if (value < mLower) return false; break; case Type_Open: if (value <= mLower) return false; break; case Type_Infinite: break; } switch (mUpperType) { case Type_Closed: if (value > mUpper) return false; break; case Type_Open: if (value >= mUpper) return false; break; case Type_Infinite: break; } return true; } std::vector CSMFilter::ValueNode::getReferencedColumns() const { return std::vector(1, mColumnId); } std::string CSMFilter::ValueNode::toString(bool numericColumns) const { std::ostringstream stream; stream << "value ("; if (numericColumns) stream << mColumnId; else stream << "\"" << CSMWorld::Columns::getName(static_cast(mColumnId)) << "\""; stream << ", "; 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; } stream << ", "; switch (mUpperType) { case Type_Closed: stream << mUpper << "]"; break; case Type_Open: stream << mUpper << ")"; break; case Type_Infinite: stream << ")"; break; } } stream << ")"; return stream.str(); } openmw-openmw-0.49.0/apps/opencs/model/filter/valuenode.hpp000066400000000000000000000025441503074453300237370ustar00rootroot00000000000000#ifndef CSM_FILTER_VALUENODE_H #define CSM_FILTER_VALUENODE_H #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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/000077500000000000000000000000001503074453300210715ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/prefs/boolsetting.cpp000066400000000000000000000025051503074453300241300ustar00rootroot00000000000000#include "boolsetting.hpp" #include #include #include #include #include "category.hpp" #include "state.hpp" 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) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::BoolSetting::makeWidgets(QWidget* parent) { 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); } connect(mWidget, &QCheckBox::stateChanged, this, &BoolSetting::valueChanged); return SettingWidgets{ .mLabel = nullptr, .mInput = mWidget }; } void CSMPrefs::BoolSetting::updateWidget() { if (mWidget) { mWidget->setCheckState(getValue() ? Qt::Checked : Qt::Unchecked); } } void CSMPrefs::BoolSetting::valueChanged(int value) { setValue(value != Qt::Unchecked); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/boolsetting.hpp000066400000000000000000000013721503074453300241360ustar00rootroot00000000000000#ifndef CSM_PREFS_BOOLSETTING_H #define CSM_PREFS_BOOLSETTING_H #include "setting.hpp" #include #include class QCheckBox; namespace CSMPrefs { class Category; class BoolSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; QCheckBox* mWidget; public: explicit BoolSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); BoolSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void valueChanged(int value); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/category.cpp000066400000000000000000000024601503074453300234140ustar00rootroot00000000000000 #include "category.hpp" #include #include "setting.hpp" #include "state.hpp" #include "subcategory.hpp" 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 { return mParent; } void CSMPrefs::Category::addSetting(Setting* 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() { return mSettings.begin(); } CSMPrefs::Category::Iterator CSMPrefs::Category::end() { return mSettings.end(); } CSMPrefs::Setting& CSMPrefs::Category::operator[](const std::string& key) { const auto it = mIndex.find(key); if (it != mIndex.end()) return *it->second; 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); } openmw-openmw-0.49.0/apps/opencs/model/prefs/category.hpp000066400000000000000000000016011503074453300234150ustar00rootroot00000000000000#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: typedef std::vector Container; typedef Container::iterator Iterator; private: State* mParent; std::string mKey; Container mSettings; std::unordered_map mIndex; public: Category(State* parent, const std::string& key); const std::string& getKey() const; State* getState() const; void addSetting(Setting* setting); void addSubcategory(Subcategory* setting); Iterator begin(); Iterator end(); Setting& operator[](const std::string& key); void update(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/coloursetting.cpp000066400000000000000000000027551503074453300245070ustar00rootroot00000000000000#include "coloursetting.hpp" #include #include #include #include #include #include "../../view/widget/coloreditor.hpp" #include "category.hpp" #include "state.hpp" 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) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::ColourSetting::makeWidgets(QWidget* parent) { QLabel* label = new QLabel(getLabel(), parent); mWidget = new CSVWidget::ColorEditor(toColor(), parent); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8(mTooltip.c_str()); label->setToolTip(tooltip); mWidget->setToolTip(tooltip); } connect(mWidget, &CSVWidget::ColorEditor::pickingFinished, this, &ColourSetting::valueChanged); return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::ColourSetting::updateWidget() { if (mWidget) mWidget->setColor(toColor()); } void CSMPrefs::ColourSetting::valueChanged() { CSVWidget::ColorEditor& widget = dynamic_cast(*sender()); setValue(widget.color().name().toStdString()); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/coloursetting.hpp000066400000000000000000000016041503074453300245040ustar00rootroot00000000000000#ifndef CSM_PREFS_COLOURSETTING_H #define CSM_PREFS_COLOURSETTING_H #include "setting.hpp" #include #include #include #include class QMutex; class QObject; class QWidget; namespace CSVWidget { class ColorEditor; } namespace CSMPrefs { class Category; class ColourSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; CSVWidget::ColorEditor* mWidget; public: explicit ColourSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ColourSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void valueChanged(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/doublesetting.cpp000066400000000000000000000040071503074453300244460ustar00rootroot00000000000000 #include "doublesetting.hpp" #include #include #include #include #include #include #include "category.hpp" #include "state.hpp" 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) { mPrecision = precision; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setRange(double min, double max) { mMin = min; mMax = max; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMin(double min) { mMin = min; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setMax(double max) { mMax = max; return *this; } CSMPrefs::DoubleSetting& CSMPrefs::DoubleSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::DoubleSetting::makeWidgets(QWidget* parent) { QLabel* label = new QLabel(getLabel(), parent); mWidget = new QDoubleSpinBox(parent); mWidget->setDecimals(mPrecision); mWidget->setRange(mMin, mMax); mWidget->setValue(getValue()); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8(mTooltip.c_str()); label->setToolTip(tooltip); mWidget->setToolTip(tooltip); } connect(mWidget, qOverload(&QDoubleSpinBox::valueChanged), this, &DoubleSetting::valueChanged); return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::DoubleSetting::updateWidget() { if (mWidget) mWidget->setValue(getValue()); } void CSMPrefs::DoubleSetting::valueChanged(double value) { setValue(value); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/doublesetting.hpp000066400000000000000000000020651503074453300244550ustar00rootroot00000000000000#ifndef CSM_PREFS_DOUBLESETTING_H #define CSM_PREFS_DOUBLESETTING_H #include "setting.hpp" class QDoubleSpinBox; namespace CSMPrefs { class Category; class DoubleSetting final : public TypedSetting { Q_OBJECT int mPrecision; double mMin; double mMax; std::string mTooltip; QDoubleSpinBox* mWidget; public: explicit DoubleSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); DoubleSetting& setPrecision(int precision); // defaults to [0, std::numeric_limits::max()] DoubleSetting& setRange(double min, double max); DoubleSetting& setMin(double min); DoubleSetting& setMax(double max); DoubleSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void valueChanged(double value); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/enumsetting.cpp000066400000000000000000000044551503074453300241470ustar00rootroot00000000000000#include "enumsetting.hpp" #include #include #include #include #include #include #include #include #include "category.hpp" #include "state.hpp" 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) { } CSMPrefs::EnumSetting& CSMPrefs::EnumSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::EnumSetting::makeWidgets(QWidget* parent) { QLabel* label = new QLabel(getLabel(), parent); mWidget = new QComboBox(parent); for (std::size_t i = 0; i < mValues.size(); ++i) { const EnumValueView& v = mValues[i]; mWidget->addItem(QString::fromUtf8(v.mValue.data(), static_cast(v.mValue.size()))); if (!v.mTooltip.empty()) mWidget->setItemData(static_cast(i), QString::fromUtf8(v.mTooltip.data(), static_cast(v.mTooltip.size())), Qt::ToolTipRole); } 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); } connect(mWidget, qOverload(&QComboBox::currentIndexChanged), this, &EnumSetting::valueChanged); return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::EnumSetting::updateWidget() { if (mWidget) mWidget->setCurrentIndex(mWidget->findText(QString::fromStdString(getValue()))); } void CSMPrefs::EnumSetting::valueChanged(int value) { if (value < 0 || static_cast(value) >= mValues.size()) throw std::logic_error("Invalid enum setting \"" + getKey() + "\" value index: " + std::to_string(value)); setValue(std::string(mValues[value].mValue)); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/enumsetting.hpp000066400000000000000000000016551503074453300241530ustar00rootroot00000000000000#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 { class Category; class EnumSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; std::span mValues; QComboBox* mWidget; public: explicit EnumSetting(Category* parent, QMutex* mutex, std::string_view key, const QString& label, std::span values, Settings::Index& index); EnumSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void valueChanged(int value); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/enumvalueview.hpp000066400000000000000000000004201503074453300244720ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/model/prefs/intsetting.cpp000066400000000000000000000033701503074453300237700ustar00rootroot00000000000000 #include "intsetting.hpp" #include #include #include #include #include #include #include "category.hpp" #include "state.hpp" 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) { mMin = min; mMax = max; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMin(int min) { mMin = min; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setMax(int max) { mMax = max; return *this; } CSMPrefs::IntSetting& CSMPrefs::IntSetting::setTooltip(const std::string& tooltip) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::IntSetting::makeWidgets(QWidget* parent) { QLabel* label = new QLabel(getLabel(), parent); 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); } connect(mWidget, qOverload(&QSpinBox::valueChanged), this, &IntSetting::valueChanged); return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::IntSetting::updateWidget() { if (mWidget) mWidget->setValue(getValue()); } void CSMPrefs::IntSetting::valueChanged(int value) { setValue(value); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/intsetting.hpp000066400000000000000000000017251503074453300237770ustar00rootroot00000000000000#ifndef CSM_PREFS_INTSETTING_H #define CSM_PREFS_INTSETTING_H #include "setting.hpp" class QSpinBox; class QMutex; class QObject; class QWidget; namespace CSMPrefs { class Category; class IntSetting final : public TypedSetting { Q_OBJECT int mMin; int mMax; std::string mTooltip; QSpinBox* mWidget; 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); IntSetting& setMin(int min); IntSetting& setMax(int max); IntSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void valueChanged(int value); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/modifiersetting.cpp000066400000000000000000000104051503074453300247710ustar00rootroot00000000000000#include "modifiersetting.hpp" #include #include #include #include #include #include #include #include #include "shortcutmanager.hpp" #include "state.hpp" class QObject; class QWidget; namespace CSMPrefs { 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) { } 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(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); widget->installEventFilter(this); // right clicking on button sets shortcut to RMB, so context menu should not appear widget->setContextMenuPolicy(Qt::PreventContextMenu); mButton = widget; connect(widget, &QPushButton::toggled, this, &ModifierSetting::buttonToggled); return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ModifierSetting::updateWidget() { if (mButton) { const std::string& shortcut = getValue(); int modifier; State::get().getShortcutManager().convertFromString(shortcut, modifier); State::get().getShortcutManager().setModifier(getKey(), modifier); resetState(); } } bool ModifierSetting::eventFilter(QObject* target, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key); } else if (event->type() == QEvent::MouseButtonPress) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int button = mouseEvent->button(); return handleEvent(target, mod, button); } else if (event->type() == QEvent::FocusOut) { resetState(); } return false; } bool ModifierSetting::handleEvent(QObject* target, int mod, int value) { // For potential future exceptions const int Blacklist[] = { 0 }; const size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); if (!mEditorActive) { if (value == Qt::RightButton) { // Clear modifier int modifier = 0; storeValue(modifier); resetState(); } return false; } // Handle blacklist for (size_t i = 0; i < BlacklistSize; ++i) { if (value == Blacklist[i]) return true; } // Update modifier int modifier = value; storeValue(modifier); resetState(); return true; } void ModifierSetting::storeValue(int modifier) { State::get().getShortcutManager().setModifier(getKey(), modifier); setValue(State::get().getShortcutManager().convertToString(modifier)); getParent()->getState()->update(*this); } void ModifierSetting::resetState() { mButton->setChecked(false); mEditorActive = false; // Button text int modifier = 0; State::get().getShortcutManager().getModifier(getKey(), modifier); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(modifier).c_str()); mButton->setText(text); } void ModifierSetting::buttonToggled(bool checked) { if (checked) mButton->setText("Press keys or click here..."); mEditorActive = checked; } } openmw-openmw-0.49.0/apps/opencs/model/prefs/modifiersetting.hpp000066400000000000000000000017161503074453300250030ustar00rootroot00000000000000#ifndef CSM_PREFS_MODIFIERSETTING_H #define CSM_PREFS_MODIFIERSETTING_H #include "setting.hpp" #include #include class QMutex; class QObject; class QWidget; class QEvent; class QPushButton; namespace CSMPrefs { class Category; class ModifierSetting final : public TypedSetting { Q_OBJECT public: explicit ModifierSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; protected: bool eventFilter(QObject* target, QEvent* event) override; private: bool handleEvent(QObject* target, int mod, int value); void storeValue(int modifier); void resetState(); QPushButton* mButton; bool mEditorActive; private slots: void buttonToggled(bool checked); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/setting.cpp000066400000000000000000000025051503074453300232540ustar00rootroot00000000000000 #include "setting.hpp" #include #include #include #include #include "category.hpp" #include "state.hpp" QMutex* CSMPrefs::Setting::getMutex() { return mMutex; } 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 { return mParent; } const std::string& CSMPrefs::Setting::getKey() const { return mKey; } QColor CSMPrefs::Setting::toColor() const { // toString() handles lock return QColor(QString::fromUtf8(toString().c_str())); } bool CSMPrefs::operator==(const Setting& setting, const std::string& key) { std::string fullKey = setting.getParent()->getKey() + "/" + setting.getKey(); return fullKey == key; } bool CSMPrefs::operator==(const std::string& key, const Setting& setting) { return setting == key; } bool CSMPrefs::operator!=(const Setting& setting, const std::string& key) { return !(setting == key); } bool CSMPrefs::operator!=(const std::string& key, const Setting& setting) { return !(key == setting); } openmw-openmw-0.49.0/apps/opencs/model/prefs/setting.hpp000066400000000000000000000055141503074453300232640ustar00rootroot00000000000000#ifndef CSM_PREFS_SETTING_H #define CSM_PREFS_SETTING_H #include #include #include #include #include #include "category.hpp" class QWidget; class QColor; class QMutex; class QGridLayout; class QLabel; namespace CSMPrefs { struct SettingWidgets { QLabel* mLabel; QWidget* mInput; }; class Setting : public QObject { Q_OBJECT Category* mParent; QMutex* mMutex; std::string mKey; QString mLabel; Settings::Index& mIndex; protected: QMutex* getMutex(); template void resetValueImpl() { QMutexLocker lock(mMutex); return mIndex.get(mParent->getKey(), mKey).reset(); } template T getValueImpl() const { QMutexLocker lock(mMutex); return mIndex.get(mParent->getKey(), mKey).get(); } template void setValueImpl(const T& value) { QMutexLocker lock(mMutex); return mIndex.get(mParent->getKey(), mKey).set(value); } public: explicit Setting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); ~Setting() override = default; 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() = 0; virtual void reset() = 0; const Category* getParent() const; const std::string& getKey() const; const QString& getLabel() const { return mLabel; } 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; void reset() final { resetValueImpl(); updateWidget(); } T getValue() const { return getValueImpl(); } 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); } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcut.cpp000066400000000000000000000123231503074453300234510ustar00rootroot00000000000000#include "shortcut.hpp" #include #include #include #include #include #include "shortcutmanager.hpp" #include "state.hpp" namespace CSMPrefs { Shortcut::Shortcut(const std::string& name, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName("") , mSecondaryMode(SM_Ignore) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); } Shortcut::Shortcut(const std::string& name, const std::string& modName, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName(modName) , mSecondaryMode(SM_Ignore) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); State::get().getShortcutManager().getModifier(modName, mModifier); } Shortcut::Shortcut(const std::string& name, const std::string& modName, SecondaryMode secMode, QWidget* parent) : QObject(parent) , mEnabled(true) , mName(name) , mModName(modName) , mSecondaryMode(secMode) , mModifier(0) , mCurrentPos(0) , mLastPos(0) , mActivationStatus(AS_Inactive) , mModifierStatus(false) , mAction(nullptr) { assert(parent); State::get().getShortcutManager().addShortcut(this); State::get().getShortcutManager().getSequence(name, mSequence); State::get().getShortcutManager().getModifier(modName, mModifier); } Shortcut::~Shortcut() { try { State::get().getShortcutManager().removeShortcut(this); } catch (const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } bool Shortcut::isEnabled() const { return mEnabled; } const std::string& Shortcut::getName() const { return mName; } const std::string& Shortcut::getModifierName() const { return mModName; } Shortcut::SecondaryMode Shortcut::getSecondaryMode() const { return mSecondaryMode; } const QKeySequence& Shortcut::getSequence() const { return mSequence; } int Shortcut::getModifier() const { return mModifier; } int Shortcut::getPosition() const { return mCurrentPos; } int Shortcut::getLastPosition() const { return mLastPos; } Shortcut::ActivationStatus Shortcut::getActivationStatus() const { return mActivationStatus; } bool Shortcut::getModifierStatus() const { return mModifierStatus; } void Shortcut::enable(bool state) { mEnabled = state; } void Shortcut::setSequence(const QKeySequence& sequence) { mSequence = sequence; mCurrentPos = 0; mLastPos = sequence.count() - 1; if (mAction) { mAction->setText(mActionText + "\t" + State::get().getShortcutManager().convertToString(mSequence).data()); } } void Shortcut::setModifier(int modifier) { mModifier = modifier; } void Shortcut::setPosition(int pos) { mCurrentPos = pos; } void Shortcut::setActivationStatus(ActivationStatus status) { mActivationStatus = status; } void Shortcut::setModifierStatus(bool status) { mModifierStatus = status; } void Shortcut::associateAction(QAction* action) { if (mAction) { mAction->setText(mActionText); disconnect(this, qOverload<>(&Shortcut::activated), mAction, &QAction::trigger); disconnect(mAction, &QAction::destroyed, this, &Shortcut::actionDeleted); } mAction = action; if (mAction) { mActionText = mAction->text(); mAction->setText(mActionText + "\t" + State::get().getShortcutManager().convertToString(mSequence).data()); connect(this, qOverload<>(&Shortcut::activated), mAction, &QAction::trigger); connect(mAction, &QAction::destroyed, this, &Shortcut::actionDeleted); } } void Shortcut::signalActivated(bool state) { emit activated(state); } void Shortcut::signalActivated() { emit activated(); } void Shortcut::signalSecondary(bool state) { emit secondary(state); } void Shortcut::signalSecondary() { emit secondary(); } QString Shortcut::toString() const { return QString(State::get().getShortcutManager().convertToString(mSequence, mModifier).data()); } void Shortcut::actionDeleted() { mAction = nullptr; } } openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcut.hpp000066400000000000000000000060171503074453300234610ustar00rootroot00000000000000#ifndef CSM_PREFS_SHORTCUT_H #define CSM_PREFS_SHORTCUT_H #include #include #include #include class QAction; class QWidget; namespace CSMPrefs { /// A class similar in purpose to QShortcut, but with the ability to use mouse buttons class Shortcut : public QObject { Q_OBJECT public: 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 }; 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(); bool isEnabled() const; const std::string& getName() const; const std::string& getModifierName() const; SecondaryMode getSecondaryMode() const; const QKeySequence& getSequence() const; int getModifier() const; /// The position in the sequence int getPosition() const; /// The position in the sequence int getLastPosition() const; ActivationStatus getActivationStatus() const; bool getModifierStatus() const; void enable(bool state); void setSequence(const QKeySequence& sequence); void setModifier(int modifier); /// The position in the sequence void setPosition(int pos); 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); // Workaround for Qt4 signals being "protected" void signalActivated(bool state); void signalActivated(); void signalSecondary(bool state); void signalSecondary(); QString toString() const; private: bool mEnabled; std::string mName; std::string mModName; SecondaryMode mSecondaryMode; QKeySequence mSequence; int mModifier; int mCurrentPos; int mLastPos; ActivationStatus mActivationStatus; bool mModifierStatus; QAction* mAction; QString mActionText; private slots: void actionDeleted(); signals: /// Triggered when the shortcut is activated or deactivated; can be determined from \p state void activated(bool state); /// Convenience signal. void activated(); /// Triggered depending on SecondaryMode void secondary(bool state); /// Convenience signal. void secondary(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcuteventhandler.cpp000066400000000000000000000272131503074453300260550ustar00rootroot00000000000000#include "shortcuteventhandler.hpp" #include #include #include #include #include #include #include "shortcut.hpp" namespace CSMPrefs { ShortcutEventHandler::ShortcutEventHandler(QObject* parent) : QObject(parent) { } void ShortcutEventHandler::addShortcut(Shortcut* shortcut) { // Enforced by shortcut class QWidget* widget = static_cast(shortcut->parent()); // Check if widget setup is needed ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); if (shortcutListIt == mWidgetShortcuts.end()) { // Create list shortcutListIt = mWidgetShortcuts.insert(std::make_pair(widget, ShortcutList())).first; // Check if widget has a parent with shortcuts, unfortunately it is not typically set yet updateParent(widget); // Intercept widget events widget->installEventFilter(this); connect(widget, &QWidget::destroyed, this, &ShortcutEventHandler::widgetDestroyed); } // Add to list shortcutListIt->second.push_back(shortcut); } void ShortcutEventHandler::removeShortcut(Shortcut* shortcut) { // Enforced by shortcut class QWidget* widget = static_cast(shortcut->parent()); 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()); } } bool ShortcutEventHandler::eventFilter(QObject* watched, QEvent* event) { // Process event if (event->type() == QEvent::KeyPress) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); unsigned int mod = (unsigned int)keyEvent->modifiers(); unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) return activate(widget, mod, key); } else if (event->type() == QEvent::KeyRelease) { QWidget* widget = static_cast(watched); QKeyEvent* keyEvent = static_cast(event); unsigned int mod = (unsigned int)keyEvent->modifiers(); unsigned int key = (unsigned int)keyEvent->key(); if (!keyEvent->isAutoRepeat()) return deactivate(widget, mod, key); } else if (event->type() == QEvent::MouseButtonPress) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); unsigned int mod = (unsigned int)mouseEvent->modifiers(); unsigned int button = (unsigned int)mouseEvent->button(); return activate(widget, mod, button); } else if (event->type() == QEvent::MouseButtonRelease) { QWidget* widget = static_cast(watched); QMouseEvent* mouseEvent = static_cast(event); unsigned int mod = (unsigned int)mouseEvent->modifiers(); unsigned int button = (unsigned int)mouseEvent->button(); return deactivate(widget, mod, button); } else if (event->type() == QEvent::FocusOut) { 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) { Shortcut* shortcut = *it; shortcut->setPosition(0); shortcut->setModifierStatus(false); if (shortcut->getActivationStatus() == Shortcut::AS_Regular) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalActivated(false); } else if (shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalSecondary(false); } } } else if (event->type() == QEvent::FocusIn) { QWidget* widget = static_cast(watched); updateParent(widget); } return false; } void ShortcutEventHandler::updateParent(QWidget* widget) { QWidget* parent = widget->parentWidget(); while (parent) { ShortcutMap::iterator parentIt = mWidgetShortcuts.find(parent); if (parentIt != mWidgetShortcuts.end()) { mChildParentRelations.insert(std::make_pair(widget, parent)); updateParent(parent); break; } // Check next parent = parent->parentWidget(); } } bool ShortcutEventHandler::activate(QWidget* widget, unsigned int mod, unsigned int button) { std::vector> potentials; bool used = false; while (widget) { ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); assert(shortcutListIt != mWidgetShortcuts.end()); // Find potential activations for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) { Shortcut* shortcut = *it; if (!shortcut->isEnabled()) continue; if (checkModifier(mod, button, shortcut, true)) used = true; if (shortcut->getActivationStatus() != Shortcut::AS_Inactive) continue; int pos = shortcut->getPosition(); int lastPos = shortcut->getLastPosition(); MatchResult result = match(mod, button, shortcut->getSequence()[pos]); if (result == Matches_WithMod || result == Matches_NoMod) { if (pos < lastPos && (result == Matches_WithMod || pos > 0)) { shortcut->setPosition(pos + 1); } else if (pos == lastPos) { potentials.emplace_back(result, shortcut); } } } // Move on to parent WidgetMap::iterator widgetIt = mChildParentRelations.find(widget); widget = (widgetIt != mChildParentRelations.end()) ? widgetIt->second : 0; } // Only activate the best match; in exact conflicts, this will favor the first shortcut added. if (!potentials.empty()) { std::stable_sort(potentials.begin(), potentials.end(), ShortcutEventHandler::sort); Shortcut* shortcut = potentials.front().second; if (shortcut->getModifierStatus() && shortcut->getSecondaryMode() == Shortcut::SM_Replace) { shortcut->setActivationStatus(Shortcut::AS_Secondary); shortcut->signalSecondary(true); shortcut->signalSecondary(); } else { shortcut->setActivationStatus(Shortcut::AS_Regular); shortcut->signalActivated(true); shortcut->signalActivated(); } used = true; } return used; } bool ShortcutEventHandler::deactivate(QWidget* widget, unsigned int mod, unsigned int button) { const int KeyMask = 0x01FFFFFF; bool used = false; while (widget) { ShortcutMap::iterator shortcutListIt = mWidgetShortcuts.find(widget); assert(shortcutListIt != mWidgetShortcuts.end()); for (ShortcutList::iterator it = shortcutListIt->second.begin(); it != shortcutListIt->second.end(); ++it) { Shortcut* shortcut = *it; if (checkModifier(mod, button, shortcut, false)) used = true; int pos = shortcut->getPosition(); MatchResult result = match(0, button, shortcut->getSequence()[pos] & KeyMask); if (result != Matches_Not) { shortcut->setPosition(0); if (shortcut->getActivationStatus() == Shortcut::AS_Regular) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalActivated(false); used = true; } else if (shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->signalSecondary(false); used = true; } } } // Move on to parent WidgetMap::iterator widgetIt = mChildParentRelations.find(widget); widget = (widgetIt != mChildParentRelations.end()) ? widgetIt->second : 0; } return used; } 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) return false; MatchResult result = match(mod, button, shortcut->getModifier()); bool used = false; if (result != Matches_Not) { shortcut->setModifierStatus(activate); if (shortcut->getSecondaryMode() == Shortcut::SM_Detach) { if (activate) { shortcut->signalSecondary(true); shortcut->signalSecondary(); } else { shortcut->signalSecondary(false); } } else if (!activate && shortcut->getActivationStatus() == Shortcut::AS_Secondary) { shortcut->setActivationStatus(Shortcut::AS_Inactive); shortcut->setPosition(0); shortcut->signalSecondary(false); used = true; } } return used; } ShortcutEventHandler::MatchResult ShortcutEventHandler::match( unsigned int mod, unsigned int button, unsigned int value) { if ((mod | button) == value) { return Matches_WithMod; } else if (button == value) { return Matches_NoMod; } else { return Matches_Not; } } bool ShortcutEventHandler::sort( const std::pair& left, const std::pair& right) { if (left.first == Matches_WithMod && right.first == Matches_NoMod) return true; else return left.second->getPosition() > right.second->getPosition(); } void ShortcutEventHandler::widgetDestroyed() { QWidget* widget = static_cast(sender()); mWidgetShortcuts.erase(widget); mChildParentRelations.erase(widget); } } openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcuteventhandler.hpp000066400000000000000000000031761503074453300260640ustar00rootroot00000000000000#ifndef CSM_PREFS_SHORTCUT_EVENT_HANDLER_H #define CSM_PREFS_SHORTCUT_EVENT_HANDLER_H #include #include #include class QEvent; class QWidget; namespace CSMPrefs { class Shortcut; /// Users of this class should install it as an event handler class ShortcutEventHandler : public QObject { Q_OBJECT public: ShortcutEventHandler(QObject* parent); void addShortcut(Shortcut* shortcut); void removeShortcut(Shortcut* shortcut); protected: bool eventFilter(QObject* watched, QEvent* event) override; private: typedef std::vector ShortcutList; // Child, Parent typedef std::map WidgetMap; typedef std::map ShortcutMap; enum MatchResult { Matches_WithMod, Matches_NoMod, Matches_Not }; void updateParent(QWidget* widget); bool activate(QWidget* widget, unsigned int mod, unsigned int button); bool deactivate(QWidget* widget, unsigned int mod, unsigned int button); bool checkModifier(unsigned int mod, unsigned int button, Shortcut* shortcut, bool activate); MatchResult match(unsigned int mod, unsigned int button, unsigned int value); // 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcutmanager.cpp000066400000000000000000001050561503074453300250120ustar00rootroot00000000000000#include "shortcutmanager.hpp" #include #include #include #include #include "shortcut.hpp" #include "shortcuteventhandler.hpp" namespace CSMPrefs { ShortcutManager::ShortcutManager() { createLookupTables(); mEventHandler = new ShortcutEventHandler(this); } void ShortcutManager::addShortcut(Shortcut* shortcut) { mShortcuts.insert(std::make_pair(shortcut->getName(), shortcut)); mShortcuts.insert(std::make_pair(shortcut->getModifierName(), shortcut)); mEventHandler->addShortcut(shortcut); } void ShortcutManager::removeShortcut(Shortcut* shortcut) { std::pair range = mShortcuts.equal_range(shortcut->getName()); for (ShortcutMap::iterator it = range.first; it != range.second;) { if (it->second == shortcut) { mShortcuts.erase(it++); } else { ++it; } } mEventHandler->removeShortcut(shortcut); } bool ShortcutManager::getSequence(std::string_view name, QKeySequence& sequence) const { SequenceMap::const_iterator item = mSequences.find(name); if (item != mSequences.end()) { sequence = item->second; return true; } else return false; } void ShortcutManager::setSequence(std::string_view name, const QKeySequence& sequence) { // Add to map/modify SequenceMap::iterator item = mSequences.find(name); if (item != mSequences.end()) { item->second = sequence; } else { mSequences.insert(std::make_pair(name, sequence)); } // Change active shortcuts std::pair rangeS = mShortcuts.equal_range(name); for (ShortcutMap::iterator it = rangeS.first; it != rangeS.second; ++it) { it->second->setSequence(sequence); } } bool ShortcutManager::getModifier(std::string_view name, int& modifier) const { ModifierMap::const_iterator item = mModifiers.find(name); if (item != mModifiers.end()) { modifier = item->second; return true; } else return false; } void ShortcutManager::setModifier(std::string_view name, int modifier) { // Add to map/modify ModifierMap::iterator item = mModifiers.find(name); if (item != mModifiers.end()) { item->second = modifier; } else { mModifiers.insert(std::make_pair(name, modifier)); } // Change active shortcuts std::pair rangeS = mShortcuts.equal_range(name); for (ShortcutMap::iterator it = rangeS.first; it != rangeS.second; ++it) { it->second->setModifier(modifier); } } std::string ShortcutManager::convertToString(const QKeySequence& sequence) const { const int MouseKeyMask = 0x01FFFFFF; const int ModMask = 0x7E000000; std::string result; for (int i = 0; i < (int)sequence.count(); ++i) { int mods = sequence[i] & ModMask; int key = sequence[i] & MouseKeyMask; if (key) { NameMap::const_iterator searchResult = mNames.find(key); if (searchResult != mNames.end()) { if (mods && i == 0) { if (mods & Qt::ControlModifier) result.append("Ctrl+"); if (mods & Qt::ShiftModifier) result.append("Shift+"); if (mods & Qt::AltModifier) result.append("Alt+"); if (mods & Qt::MetaModifier) result.append("Meta+"); if (mods & Qt::KeypadModifier) result.append("Keypad+"); if (mods & Qt::GroupSwitchModifier) result.append("GroupSwitch+"); } else if (i > 0) { result.append("+"); } result.append(searchResult->second); } } } return result; } std::string ShortcutManager::convertToString(int modifier) const { NameMap::const_iterator searchResult = mNames.find(modifier); if (searchResult != mNames.end()) { return searchResult->second; } else return ""; } std::string ShortcutManager::convertToString(const QKeySequence& sequence, int modifier) const { std::string concat = convertToString(sequence) + ";" + convertToString(modifier); return concat; } void ShortcutManager::convertFromString(std::string_view data, QKeySequence& sequence) const { const int MaxKeys = 4; // A limitation of QKeySequence size_t end = data.find(';'); size_t size = std::min(end, data.size()); std::string_view value = data.substr(0, size); size_t start = 0; int keyPos = 0; int mods = 0; int keys[MaxKeys] = {}; while (start < value.size()) { end = data.find('+', start); end = std::min(end, value.size()); std::string_view name = value.substr(start, end - start); if (name == "Ctrl") { mods |= Qt::ControlModifier; } else if (name == "Shift") { mods |= Qt::ShiftModifier; } else if (name == "Alt") { mods |= Qt::AltModifier; } else if (name == "Meta") { mods |= Qt::MetaModifier; } else if (name == "Keypad") { mods |= Qt::KeypadModifier; } else if (name == "GroupSwitch") { mods |= Qt::GroupSwitchModifier; } else { KeyMap::const_iterator searchResult = mKeys.find(name); if (searchResult != mKeys.end()) { keys[keyPos] = mods | searchResult->second; mods = 0; keyPos += 1; if (keyPos >= MaxKeys) break; } } start = end + 1; } sequence = QKeySequence(keys[0], keys[1], keys[2], keys[3]); } void ShortcutManager::convertFromString(std::string_view data, int& modifier) const { size_t start = data.find(';') + 1; start = std::min(start, data.size()); std::string_view name = data.substr(start); KeyMap::const_iterator searchResult = mKeys.find(name); if (searchResult != mKeys.end()) { modifier = searchResult->second; } else { modifier = 0; } } void ShortcutManager::convertFromString(std::string_view data, QKeySequence& sequence, int& modifier) const { convertFromString(data, sequence); convertFromString(data, modifier); } void ShortcutManager::createLookupTables() { // Mouse buttons mNames.insert(std::make_pair(Qt::LeftButton, "LMB")); mNames.insert(std::make_pair(Qt::RightButton, "RMB")); mNames.insert(std::make_pair(Qt::MiddleButton, "MMB")); mNames.insert(std::make_pair(Qt::XButton1, "Mouse4")); mNames.insert(std::make_pair(Qt::XButton2, "Mouse5")); // Keyboard buttons for (size_t i = 0; QtKeys[i].first != 0; ++i) { mNames.insert(QtKeys[i]); } // Generate inverse map for (NameMap::const_iterator it = mNames.begin(); it != mNames.end(); ++it) { mKeys.insert(std::make_pair(it->second, it->first)); } } QString ShortcutManager::processToolTip(const QString& toolTip) const { const QChar SequenceStart = '{'; const QChar SequenceEnd = '}'; QStringList substrings; int prevIndex = 0; int startIndex = toolTip.indexOf(SequenceStart); int endIndex = (startIndex != -1) ? toolTip.indexOf(SequenceEnd, startIndex) : -1; // Process every valid shortcut escape sequence while (startIndex != -1 && endIndex != -1) { int count = startIndex - prevIndex; if (count > 0) { substrings.push_back(toolTip.mid(prevIndex, count)); } // Find sequence name startIndex += 1; // '{' character count = endIndex - startIndex; if (count > 0) { QString settingName = toolTip.mid(startIndex, count); QKeySequence sequence; int modifier; if (getSequence(settingName.toUtf8().data(), sequence)) { QString value = QString::fromUtf8(convertToString(sequence).c_str()); substrings.push_back(value); } else if (getModifier(settingName.toUtf8().data(), modifier)) { QString value = QString::fromUtf8(convertToString(modifier).c_str()); substrings.push_back(value); } prevIndex = endIndex + 1; // '}' character } startIndex = toolTip.indexOf(SequenceStart, endIndex); endIndex = (startIndex != -1) ? toolTip.indexOf(SequenceEnd, startIndex) : -1; } if (prevIndex < toolTip.size()) { substrings.push_back(toolTip.mid(prevIndex)); } 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, static_cast(nullptr)), }; } openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcutmanager.hpp000066400000000000000000000043731503074453300250170ustar00rootroot00000000000000#ifndef CSM_PREFS_SHORTCUTMANAGER_H #define CSM_PREFS_SHORTCUTMANAGER_H #include #include #include #include #include #include namespace CSMPrefs { class Shortcut; class ShortcutEventHandler; /// Class used to track and update shortcuts/sequences class ShortcutManager : public QObject { Q_OBJECT public: ShortcutManager(); /// The shortcut class will do this automatically void addShortcut(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 getModifier(std::string_view name, int& modifier) const; void setModifier(std::string_view name, int modifier); std::string convertToString(const QKeySequence& sequence) const; std::string convertToString(int modifier) const; std::string convertToString(const QKeySequence& sequence, int modifier) const; void convertFromString(std::string_view data, QKeySequence& sequence) const; void convertFromString(std::string_view data, int& modifier) const; void convertFromString(std::string_view data, QKeySequence& sequence, int& modifier) const; /// Replaces "{sequence-name}" or "{modifier-name}" with the appropriate text QString processToolTip(const QString& toolTip) const; 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; ShortcutMap mShortcuts; SequenceMap mSequences; ModifierMap mModifiers; NameMap mNames; KeyMap mKeys; ShortcutEventHandler* mEventHandler; void createLookupTables(); static const std::pair QtKeys[]; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcutsetting.cpp000066400000000000000000000133271503074453300250540ustar00rootroot00000000000000#include "shortcutsetting.hpp" #include #include #include #include #include #include #include #include #include #include #include "shortcutmanager.hpp" #include "state.hpp" namespace CSMPrefs { 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) { for (int i = 0; i < MaxKeys; ++i) { mEditorKeys[i] = 0; } } 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(getLabel(), parent); QPushButton* widget = new QPushButton(text, parent); widget->setCheckable(true); widget->installEventFilter(this); // right clicking on button sets shortcut to RMB, so context menu should not appear widget->setContextMenuPolicy(Qt::PreventContextMenu); mButton = widget; connect(widget, &QPushButton::toggled, this, &ShortcutSetting::buttonToggled); return SettingWidgets{ .mLabel = label, .mInput = widget }; } void ShortcutSetting::updateWidget() { if (mButton) { const std::string shortcut = getValue(); QKeySequence sequence; State::get().getShortcutManager().convertFromString(shortcut, sequence); State::get().getShortcutManager().setSequence(getKey(), sequence); resetState(); } } bool ShortcutSetting::eventFilter(QObject* target, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key, true); } else if (event->type() == QEvent::KeyRelease) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->isAutoRepeat()) return true; int mod = keyEvent->modifiers(); int key = keyEvent->key(); return handleEvent(target, mod, key, false); } else if (event->type() == QEvent::MouseButtonPress) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int key = mouseEvent->button(); return handleEvent(target, mod, key, true); } else if (event->type() == QEvent::MouseButtonRelease) { QMouseEvent* mouseEvent = static_cast(event); int mod = mouseEvent->modifiers(); int key = mouseEvent->button(); return handleEvent(target, mod, key, false); } else if (event->type() == QEvent::FocusOut) { resetState(); } return false; } 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 size_t BlacklistSize = sizeof(Blacklist) / sizeof(int); if (!mEditorActive) { if (value == Qt::RightButton && !active) { // Clear sequence QKeySequence sequence = QKeySequence(0, 0, 0, 0); storeValue(sequence); resetState(); } return false; } // Handle blacklist for (size_t i = 0; i < BlacklistSize; ++i) { if (value == Blacklist[i]) return true; } if (!active || mEditorPos >= MaxKeys) { // Update key QKeySequence sequence = QKeySequence(mEditorKeys[0], mEditorKeys[1], mEditorKeys[2], mEditorKeys[3]); storeValue(sequence); resetState(); } else { if (mEditorPos == 0) { mEditorKeys[0] = mod | value; } else { mEditorKeys[mEditorPos] = value; } mEditorPos += 1; } return true; } void ShortcutSetting::storeValue(const QKeySequence& sequence) { State::get().getShortcutManager().setSequence(getKey(), sequence); setValue(State::get().getShortcutManager().convertToString(sequence)); getParent()->getState()->update(*this); } void ShortcutSetting::resetState() { mButton->setChecked(false); mEditorActive = false; mEditorPos = 0; for (int i = 0; i < MaxKeys; ++i) { mEditorKeys[i] = 0; } // Button text QKeySequence sequence; State::get().getShortcutManager().getSequence(getKey(), sequence); QString text = QString::fromUtf8(State::get().getShortcutManager().convertToString(sequence).c_str()); mButton->setText(text); } void ShortcutSetting::buttonToggled(bool checked) { if (checked) mButton->setText("Press keys or click here..."); mEditorActive = checked; } } openmw-openmw-0.49.0/apps/opencs/model/prefs/shortcutsetting.hpp000066400000000000000000000022011503074453300250460ustar00rootroot00000000000000#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 Category; class ShortcutSetting final : public TypedSetting { Q_OBJECT public: explicit ShortcutSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; protected: bool eventFilter(QObject* target, QEvent* event) override; private: bool handleEvent(QObject* target, int mod, int value, bool active); void storeValue(const QKeySequence& sequence); void resetState(); static constexpr int MaxKeys = 4; QPushButton* mButton; bool mEditorActive; int mEditorPos; int mEditorKeys[MaxKeys]; private slots: void buttonToggled(bool checked); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/state.cpp000066400000000000000000001012061503074453300227150ustar00rootroot00000000000000#include "state.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "boolsetting.hpp" #include "coloursetting.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::declare() { 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( "Whether a newly open top level window will show status bars. " " 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 exists, reuse the existing subview."); 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, "Main Window Horizontal Scrollbar Mode"); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) declareBool(mValues->mWindows.mGrowLimit, "Grow Limit Screen") .setTooltip( "When \"Grow then Scroll\" option is selected, the window size grows to" " the width of the virtual desktop. \nIf this option is selected the the window growth" "is limited to the current screen."); #endif declareCategory("Records"); declareEnum(mValues->mRecords.mStatusFormat, "Modification Status Display Format"); 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 a 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, "Filter Apply Delay (ms)"); 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, "Max Characters Before the Search String") .setTooltip("Maximum number of characters to display in the search result before the searched text"); declareInt(mValues->mSearchAndReplace.mCharAfter, "Max Characters After the Search String") .setTooltip("Maximum number of characters to display in the search result after the searched text"); declareBool(mValues->mSearchAndReplace.mAutoDelete, "Delete Row from the Result Table After 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 that are longer than the width of the 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, "Source Error Update Delay (ms)").setRange(0, 10000); declareInt(mValues->mScripts.mErrorHeight, "Initial Error Panel Height").setRange(100, 10000); declareBool(mValues->mScripts.mHighlightOccurrences, "Highlight Selected Name Occurrences"); declareColour(mValues->mScripts.mColourHighlight, "Highlight Colour: Selected Name 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, "Dragging Mouse Sensitivity").setRange(0.001, 100.0); declareDouble(mValues->mSceneInput.mDragWheelFactor, "Dragging Mouse Wheel Sensitivity").setRange(0.001, 100.0); declareDouble(mValues->mSceneInput.mDragShiftFactor, "Dragging Shift-Acceleration Factor") .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(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."); 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 Scene Tooltips"); declareInt(mValues->mTooltips.mSceneDelay, "Tooltip Delay (ms)").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 the dropped instance cannot be placed against another object at the " "insertion point, it will be placed at this distance from the insertion point."); declareEnum(mValues->mSceneEditing.mOutsideDrop, "Instance Dropping Outside of Cells"); declareEnum(mValues->mSceneEditing.mOutsideVisibleDrop, "Instance Dropping Outside of Visible Cells"); declareEnum(mValues->mSceneEditing.mOutsideLandedit, "Terrain Editing Outside of Cells") .setTooltip("Behaviour of terrain editing if land editing brush reaches an area without a cell record."); declareEnum(mValues->mSceneEditing.mOutsideVisibleLandedit, "Terrain Editing Outside of Visible Cells") .setTooltip( "Behaviour 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 Height Painting") .setTooltip("Smooth the normally bumpy results of raise and lower tools."); declareDouble(mValues->mSceneEditing.mLandeditPostSmoothstrength, "Post-Edit Smoothing Strength") .setTooltip( "Smoothing strength for Smooth Land after Height Painting setting. " "Negative values may be used to invert the effect and make the terrain rougher.") .setMin(-1) .setMax(1); declareBool(mValues->mSceneEditing.mOpenListView, "Open Action Shows Instances Table") .setTooltip( "Opening an instance from the scene view will open the instances table instead of the record view for that " "instance."); declareEnum(mValues->mSceneEditing.mPrimarySelectAction, "Primary Select Action") .setTooltip( "Selection can be chosen between select only, add to selection, remove from selection and invert " "selection."); declareEnum(mValues->mSceneEditing.mSecondarySelectAction, "Secondary Select Action") .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.mSceneInstanceDrop, "Drop to Collision"); 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("Third person base model and animations"); declareString(mValues->mModels.mBaseanimkna, "Base Animations, Beast") .setTooltip("Third person beast race base model and animations"); declareString(mValues->mModels.mBaseanimfemale, "Base Animations, Female") .setTooltip("Third person female base model and animations"); declareString(mValues->mModels.mWolfskin, "Base Animations, Werewolf").setTooltip("Third person werewolf skin"); } void CSMPrefs::State::declareCategory(const std::string& key) { std::map::iterator iter = mCategories.find(key); if (iter != mCategories.end()) { mCurrentCategory = iter; } else { mCurrentCategory = mCategories.insert(std::make_pair(key, Category(this, key))).first; } } CSMPrefs::IntSetting& CSMPrefs::State::declareInt(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::IntSetting* setting = new CSMPrefs::IntSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::DoubleSetting& CSMPrefs::State::declareDouble(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::DoubleSetting* setting = new CSMPrefs::DoubleSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::BoolSetting& CSMPrefs::State::declareBool(Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::BoolSetting* setting = new CSMPrefs::BoolSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::EnumSetting& CSMPrefs::State::declareEnum(EnumSettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::EnumSetting* setting = new CSMPrefs::EnumSetting( &mCurrentCategory->second, &mMutex, value.getValue().mName, label, value.getEnumValues(), *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::ColourSetting& CSMPrefs::State::declareColour( Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::ColourSetting* setting = new CSMPrefs::ColourSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::ShortcutSetting& CSMPrefs::State::declareShortcut( Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); // Setup with actual data QKeySequence sequence; getShortcutManager().convertFromString(value.get(), sequence); getShortcutManager().setSequence(value.mName, sequence); CSMPrefs::ShortcutSetting* setting = new CSMPrefs::ShortcutSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::StringSetting& CSMPrefs::State::declareString( Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); CSMPrefs::StringSetting* setting = new CSMPrefs::StringSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } CSMPrefs::ModifierSetting& CSMPrefs::State::declareModifier( Settings::SettingValue& value, const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); // Setup with actual data int modifier; getShortcutManager().convertFromString(value.get(), modifier); getShortcutManager().setModifier(value.mName, modifier); CSMPrefs::ModifierSetting* setting = new CSMPrefs::ModifierSetting(&mCurrentCategory->second, &mMutex, value.mName, label, *mIndex); mCurrentCategory->second.addSetting(setting); return *setting; } void CSMPrefs::State::declareSubcategory(const QString& label) { if (mCurrentCategory == mCategories.end()) throw std::logic_error("no category for setting"); mCurrentCategory->second.addSubcategory( new CSMPrefs::Subcategory(&mCurrentCategory->second, &mMutex, label, *mIndex)); } 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"); sThis = this; declare(); } CSMPrefs::State::~State() { sThis = nullptr; } void CSMPrefs::State::save() { Settings::Manager::saveUser(mConfigurationManager.getUserConfigPath() / mConfigFile); } CSMPrefs::State::Iterator CSMPrefs::State::begin() { return mCategories.begin(); } CSMPrefs::State::Iterator CSMPrefs::State::end() { return mCategories.end(); } CSMPrefs::ShortcutManager& CSMPrefs::State::getShortcutManager() { return mShortcutManager; } CSMPrefs::Category& CSMPrefs::State::operator[](const std::string& key) { Iterator iter = mCategories.find(key); if (iter == mCategories.end()) throw std::logic_error("Invalid user settings category: " + key); return iter->second; } void CSMPrefs::State::update(const Setting& setting) { emit settingChanged(&setting); } CSMPrefs::State& CSMPrefs::State::get() { if (!sThis) throw std::logic_error("No instance of CSMPrefs::State"); return *sThis; } void CSMPrefs::State::resetCategory(const std::string& category) { Collection::iterator container = mCategories.find(category); if (container != mCategories.end()) { for (Setting* setting : container->second) { setting->reset(); update(*setting); } } } void CSMPrefs::State::resetAll() { for (Collection::iterator iter = mCategories.begin(); iter != mCategories.end(); ++iter) { resetCategory(iter->first); } } CSMPrefs::State& CSMPrefs::get() { return State::get(); } openmw-openmw-0.49.0/apps/opencs/model/prefs/state.hpp000066400000000000000000000056231503074453300227300ustar00rootroot00000000000000#ifndef CSM_PREFS_STATE_H #define CSM_PREFS_STATE_H #include #include #include #include #ifndef Q_MOC_RUN #include #endif #include "category.hpp" #include "enumsetting.hpp" #include "shortcutmanager.hpp" class QColor; namespace Settings { class Index; } namespace CSMPrefs { class IntSetting; class DoubleSetting; class BoolSetting; class ColourSetting; class ShortcutSetting; class ModifierSetting; class Setting; class StringSetting; class EnumSettingValue; struct Values; /// \brief User settings state /// /// \note Access to the user settings is thread-safe once all declarations and loading has /// 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 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 declareCategory(const std::string& key); IntSetting& declareInt(Settings::SettingValue& value, const QString& label); DoubleSetting& declareDouble(Settings::SettingValue& value, const QString& label); BoolSetting& declareBool(Settings::SettingValue& value, const QString& label); EnumSetting& declareEnum(EnumSettingValue& value, const QString& label); ColourSetting& declareColour(Settings::SettingValue& value, const QString& label); ShortcutSetting& declareShortcut(Settings::SettingValue& value, const QString& label); StringSetting& declareString(Settings::SettingValue& value, const QString& label); ModifierSetting& declareModifier(Settings::SettingValue& value, const QString& label); void declareSubcategory(const QString& label); public: State(const Files::ConfigurationManager& configurationManager); State(const State&) = delete; ~State(); State& operator=(const State&) = delete; void save(); Iterator begin(); Iterator end(); ShortcutManager& getShortcutManager(); Category& operator[](const std::string& key); void update(const Setting& setting); static State& get(); void resetCategory(const std::string& category); void resetAll(); signals: void settingChanged(const CSMPrefs::Setting* setting); }; // convenience function State& get(); } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/stringsetting.cpp000066400000000000000000000026411503074453300245040ustar00rootroot00000000000000 #include "stringsetting.hpp" #include #include #include #include #include #include "category.hpp" #include "state.hpp" 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) { mTooltip = tooltip; return *this; } CSMPrefs::SettingWidgets CSMPrefs::StringSetting::makeWidgets(QWidget* parent) { QLabel* label = new QLabel(getLabel(), parent); mWidget = new QLineEdit(QString::fromStdString(getValue()), parent); mWidget->setMinimumWidth(300); if (!mTooltip.empty()) { QString tooltip = QString::fromUtf8(mTooltip.c_str()); label->setToolTip(tooltip); mWidget->setToolTip(tooltip); } connect(mWidget, &QLineEdit::textChanged, this, &StringSetting::textChanged); return SettingWidgets{ .mLabel = label, .mInput = mWidget }; } void CSMPrefs::StringSetting::updateWidget() { if (mWidget) mWidget->setText(QString::fromStdString(getValue())); } void CSMPrefs::StringSetting::textChanged(const QString& text) { setValue(text.toStdString()); getParent()->getState()->update(*this); } openmw-openmw-0.49.0/apps/opencs/model/prefs/stringsetting.hpp000066400000000000000000000015001503074453300245020ustar00rootroot00000000000000#ifndef CSM_PREFS_StringSetting_H #define CSM_PREFS_StringSetting_H #include "setting.hpp" #include #include class QLineEdit; class QMutex; class QObject; class QWidget; namespace CSMPrefs { class Category; class StringSetting final : public TypedSetting { Q_OBJECT std::string mTooltip; QLineEdit* mWidget; public: explicit StringSetting( Category* parent, QMutex* mutex, std::string_view key, const QString& label, Settings::Index& index); StringSetting& setTooltip(const std::string& tooltip); /// Return label, input widget. SettingWidgets makeWidgets(QWidget* parent) override; void updateWidget() override; private slots: void textChanged(const QString& text); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/prefs/subcategory.cpp000066400000000000000000000006401503074453300241240ustar00rootroot00000000000000#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 }; } } openmw-openmw-0.49.0/apps/opencs/model/prefs/subcategory.hpp000066400000000000000000000010541503074453300241310ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/model/prefs/values.hpp000066400000000000000000001034621503074453300231070ustar00rootroot00000000000000#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 mSceneInstanceDrop{ mIndex, sName, "scene-instance-drop", "F" }; 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", "" }; 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", "" }; 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 openmw-openmw-0.49.0/apps/opencs/model/tools/000077500000000000000000000000001503074453300211125ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/tools/birthsigncheck.cpp000066400000000000000000000042261503074453300246110ustar00rootroot00000000000000#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) { mIgnoreBaseRecords = false; } int CSMTools::BirthsignCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mBirthsigns.getSize(); } void CSMTools::BirthsignCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mBirthsigns.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::BirthSign& birthsign = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Birthsign, birthsign.mId); if (birthsign.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (birthsign.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); if (birthsign.mTexture.empty()) messages.add(id, "Image is missing", "", CSMDoc::Message::Severity_Error); else if (mTextures.searchId(birthsign.mTexture) == -1) { 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); } /// \todo check data members that can't be edited in the table view } openmw-openmw-0.49.0/apps/opencs/model/tools/birthsigncheck.hpp000066400000000000000000000017051503074453300246150ustar00rootroot00000000000000#ifndef CSM_TOOLS_BIRTHSIGNCHECK_H #define CSM_TOOLS_BIRTHSIGNCHECK_H #include "../world/idcollection.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: BirthsignCheckStage( const CSMWorld::IdCollection& birthsigns, 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/bodypartcheck.cpp000066400000000000000000000046621503074453300244500ustar00rootroot00000000000000#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) { mIgnoreBaseRecords = false; } int CSMTools::BodyPartCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mBodyParts.getSize(); } void CSMTools::BodyPartCheckStage::perform(int stage, CSMDoc::Messages& messages) { 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(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_BodyPart, bodyPart.mId); // Check BYDT 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) messages.add(id, "Invalid type", "", CSMDoc::Message::Severity_Error); // Check MODL if (bodyPart.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); 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.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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/bodypartcheck.hpp000066400000000000000000000021321503074453300244430ustar00rootroot00000000000000#ifndef CSM_TOOLS_BODYPARTCHECK_H #define CSM_TOOLS_BODYPARTCHECK_H #include #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; public: 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; ///< Messages resulting from this tage will be appended to \a messages. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/classcheck.cpp000066400000000000000000000054521503074453300237270ustar00rootroot00000000000000#include "classcheck.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" CSMTools::ClassCheckStage::ClassCheckStage(const CSMWorld::IdCollection& classes) : mClasses(classes) { mIgnoreBaseRecords = false; } int CSMTools::ClassCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mClasses.getSize(); } void CSMTools::ClassCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mClasses.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Class& class_ = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Class, class_.mId); // A class should have a name if (class_.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // A playable class should have a description if (class_.mData.mIsPlayable != 0 && class_.mDescription.empty()) messages.add(id, "Description of a playable class is missing", "", CSMDoc::Message::Severity_Warning); // test for invalid attributes 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 { 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 (const auto& s : class_.mData.mSkills) for (int skill : s) ++skills[skill]; for (auto& skill : skills) if (skill.second > 1) { messages.add(id, "Skill " + ESM::Skill::indexToRefId(skill.first).toString() + " is listed more than once", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/classcheck.hpp000066400000000000000000000014221503074453300237250ustar00rootroot00000000000000#ifndef CSM_TOOLS_CLASSCHECK_H #define CSM_TOOLS_CLASSCHECK_H #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: ClassCheckStage(const CSMWorld::IdCollection& classes); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/effectlistcheck.cpp000066400000000000000000000100021503074453300247350ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/opencs/model/tools/effectlistcheck.hpp000066400000000000000000000010721503074453300247510ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/model/tools/enchantmentcheck.cpp000066400000000000000000000036401503074453300251230ustar00rootroot00000000000000#include "enchantmentcheck.hpp" #include #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include "effectlistcheck.hpp" CSMTools::EnchantmentCheckStage::EnchantmentCheckStage(const CSMWorld::IdCollection& enchantments) : mEnchantments(enchantments) { mIgnoreBaseRecords = false; } int CSMTools::EnchantmentCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mEnchantments.getSize(); } void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mEnchantments.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Enchantment& enchantment = record.get(); 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); if (enchantment.mData.mCost < 0) messages.add(id, "Cost is negative", "", CSMDoc::Message::Severity_Error); if (enchantment.mData.mCharge < 0) messages.add(id, "Charge is negative", "", CSMDoc::Message::Severity_Error); if (enchantment.mData.mCost > enchantment.mData.mCharge) messages.add(id, "Cost is higher than charge", "", CSMDoc::Message::Severity_Error); effectListCheck(enchantment.mEffects.mList, messages, id); } openmw-openmw-0.49.0/apps/opencs/model/tools/enchantmentcheck.hpp000066400000000000000000000014611503074453300251270ustar00rootroot00000000000000#ifndef CSM_TOOLS_ENCHANTMENTCHECK_H #define CSM_TOOLS_ENCHANTMENTCHECK_H #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); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/factioncheck.cpp000066400000000000000000000050271503074453300242430ustar00rootroot00000000000000#include "factioncheck.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" CSMTools::FactionCheckStage::FactionCheckStage(const CSMWorld::IdCollection& factions) : mFactions(factions) { mIgnoreBaseRecords = false; } int CSMTools::FactionCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mFactions.getSize(); } void CSMTools::FactionCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mFactions.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Faction& faction = record.get(); 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 std::map attributeCount; for (size_t i = 0; i < faction.mData.mAttribute.size(); ++i) { 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 skill : faction.mData.mSkills) if (skill != -1) ++skills[skill]; for (auto& skill : skills) if (skill.second > 1) { 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 } openmw-openmw-0.49.0/apps/opencs/model/tools/factioncheck.hpp000066400000000000000000000014441503074453300242470ustar00rootroot00000000000000#ifndef CSM_TOOLS_FACTIONCHECK_H #define CSM_TOOLS_FACTIONCHECK_H #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: FactionCheckStage(const CSMWorld::IdCollection& factions); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/gmstcheck.cpp000066400000000000000000000124041503074453300235670ustar00rootroot00000000000000#include "gmstcheck.hpp" #include #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" #include "../world/defaultgmsts.hpp" CSMTools::GmstCheckStage::GmstCheckStage(const CSMWorld::IdCollection& gameSettings) : mGameSettings(gameSettings) { mIgnoreBaseRecords = false; } int CSMTools::GmstCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mGameSettings.getSize(); } void CSMTools::GmstCheckStage::perform(int stage, CSMDoc::Messages& messages) { 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); 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, 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.startsWith("f")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::FloatCount; ++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"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } 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.startsWith("i")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::IntCount; ++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"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } 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.startsWith("s")) { for (size_t i = 0; i < CSMWorld::DefaultGmsts::StringCount; ++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 " << gmstIdString << " but found " << varTypeToString(gmst.mValue.getType()) << " type"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); } break; // for loop } } } } 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"; } } openmw-openmw-0.49.0/apps/opencs/model/tools/gmstcheck.hpp000066400000000000000000000016201503074453300235720ustar00rootroot00000000000000#ifndef CSM_TOOLS_GMSTCHECK_H #define CSM_TOOLS_GMSTCHECK_H #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); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/journalcheck.cpp000066400000000000000000000065571503074453300243030ustar00rootroot00000000000000#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) { mIgnoreBaseRecords = false; } int CSMTools::JournalCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); mInfosByTopic = mJournalInfos.getInfosByTopic(); return mJournals.getSize(); } void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& journalRecord = mJournals.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && journalRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || journalRecord.isDeleted()) return; const ESM::Dialogue& journal = journalRecord.get(); int statusNamedCount = 0; int totalInfoCount = 0; std::set questIndices; if (const auto infos = mInfosByTopic.find(journal.mId); infos != mInfosByTopic.end()) { for (const CSMWorld::Record* record : infos->second) { 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); } } } if (totalInfoCount == 0) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Journal, journal.mId); messages.add(id, "No related journal entry", "", CSMDoc::Message::Severity_Warning); } else if (statusNamedCount > 1) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Journal, journal.mId); messages.add(id, "Multiple entries with quest status 'Named'", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/journalcheck.hpp000066400000000000000000000020431503074453300242720ustar00rootroot00000000000000#ifndef CSM_TOOLS_JOURNALCHECK_H #define CSM_TOOLS_JOURNALCHECK_H #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); 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& mJournals; const CSMWorld::InfoCollection& mJournalInfos; bool mIgnoreBaseRecords; CSMWorld::InfosRecordPtrByTopic mInfosByTopic; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/magiceffectcheck.cpp000066400000000000000000000126551503074453300250620ustar00rootroot00000000000000#include "magiceffectcheck.hpp" #include #include #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" 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.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) { mIgnoreBaseRecords = false; } int CSMTools::MagicEffectCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mMagicEffects.getSize(); } void CSMTools::MagicEffectCheckStage::perform(int stage, CSMDoc::Messages& messages) { 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, 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()) { messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); } if (effect.mData.mBaseCost < 0.0f) { messages.add(id, "Base cost is negative", "", CSMDoc::Message::Severity_Error); } if (effect.mIcon.empty()) { messages.add(id, "Icon is missing", "", CSMDoc::Message::Severity_Error); } else { if (mIcons.searchId(effect.mIcon) == -1) { std::string ddsIcon = effect.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(id, "Icon '" + effect.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } } if (!effect.mParticle.empty()) { if (mTextures.searchId(effect.mParticle) == -1) { 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); } } if (!effect.mCasting.empty()) { const std::string error = checkObject(effect.mCasting, CSMWorld::UniversalId::Type_Static, "Casting object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mHit.empty()) { const std::string error = checkObject(effect.mHit, CSMWorld::UniversalId::Type_Static, "Hit object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mArea.empty()) { const std::string error = checkObject(effect.mArea, CSMWorld::UniversalId::Type_Static, "Area object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mBolt.empty()) { const std::string error = checkObject(effect.mBolt, CSMWorld::UniversalId::Type_Weapon, "Bolt object"); if (!error.empty()) messages.add(id, error, "", CSMDoc::Message::Severity_Error); } if (!effect.mCastSound.empty() && mSounds.searchId(effect.mCastSound) == -1) 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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!effect.mAreaSound.empty() && mSounds.searchId(effect.mAreaSound) == -1) 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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); } openmw-openmw-0.49.0/apps/opencs/model/tools/magiceffectcheck.hpp000066400000000000000000000030551503074453300250610ustar00rootroot00000000000000#ifndef CSM_TOOLS_MAGICEFFECTCHECK_HPP #define CSM_TOOLS_MAGICEFFECTCHECK_HPP #include #include #include #include "../world/idcollection.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 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/mandatoryid.cpp000066400000000000000000000015341503074453300241340ustar00rootroot00000000000000#include "mandatoryid.hpp" #include #include "../world/collectionbase.hpp" #include "../world/record.hpp" #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) { if (mIdCollection.searchId(mIds.at(stage)) == -1 || mIdCollection.getRecord(mIds.at(stage)).isDeleted()) messages.add(mCollectionId, "Missing mandatory record: " + mIds.at(stage).toDebugString()); } openmw-openmw-0.49.0/apps/opencs/model/tools/mandatoryid.hpp000066400000000000000000000017471503074453300241470ustar00rootroot00000000000000#ifndef CSM_TOOLS_MANDATORYID_H #define CSM_TOOLS_MANDATORYID_H #include #include #include "../doc/stage.hpp" #include "../world/universalid.hpp" #include namespace CSMDoc { class Messages; } namespace CSMWorld { class CollectionBase; } 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: MandatoryIdStage(const CSMWorld::CollectionBase& idCollection, const CSMWorld::UniversalId& collectionId, const std::vector& ids); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/mergeoperation.cpp000066400000000000000000000113231503074453300246360ustar00rootroot00000000000000#include "mergeoperation.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) { 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) { mState.mTarget = std::move(document); } void CSMTools::MergeOperation::operationDone() { CSMDoc::Operation::operationDone(); if (mState.mCompleted) emit mergeDone(mState.mTarget.release()); } openmw-openmw-0.49.0/apps/opencs/model/tools/mergeoperation.hpp000066400000000000000000000016221503074453300246440ustar00rootroot00000000000000#ifndef CSM_TOOLS_MERGEOPERATION_H #define CSM_TOOLS_MERGEOPERATION_H #include #include #include "../doc/operation.hpp" #include "mergestate.hpp" class QObject; namespace CSMDoc { class Document; } namespace CSMTools { class MergeOperation : public CSMDoc::Operation { Q_OBJECT MergeState mState; public: MergeOperation(CSMDoc::Document& document, ToUTF8::FromType encoding); /// \attention Do not call this function while a merge is running. void setTarget(std::unique_ptr document); protected slots: void operationDone() override; 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/mergestages.cpp000066400000000000000000000147051503074453300241330ustar00rootroot00000000000000#include "mergestages.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "mergestate.hpp" #include "../doc/document.hpp" #include "../world/commands.hpp" #include "../world/data.hpp" #include "../world/idtable.hpp" namespace CSMDoc { class Messages; } CSMTools::StartMergeStage::StartMergeStage(MergeState& state) : mState(state) { } int CSMTools::StartMergeStage::setup() { return 1; } 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) { } int CSMTools::FinishMergedDocumentStage::setup() { return 1; } 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). std::filesystem::path path = mState.mSource.getContentFiles()[0]; ESM::ESMReader reader; reader.setEncoder(&mEncoder); reader.open(path); CSMWorld::MetaData source; 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.mCompleted = true; } 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) { mState.mSource.getData().getReferenceables().copyTo(stage, mState.mTarget->getData().getReferenceables()); } CSMTools::MergeReferencesStage::MergeReferencesStage(MergeState& state) : mState(state) { } int CSMTools::MergeReferencesStage::setup() { mIndex.clear(); return mState.mSource.getData().getReferences().getSize(); } void CSMTools::MergeReferencesStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getReferences().getRecord(stage); if (!record.isDeleted()) { CSMWorld::CellRef ref = record.get(); ref.mOriginalCell = ref.mCell; ref.mRefNum.mIndex = mIndex[ref.mCell]++; ref.mRefNum.mContentFile = -1; ref.mNew = false; mState.mTarget->getData().getReferences().appendRecord(std::make_unique>( CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &ref))); } } CSMTools::PopulateLandTexturesMergeStage::PopulateLandTexturesMergeStage(MergeState& state) : mState(state) { } int CSMTools::PopulateLandTexturesMergeStage::setup() { return mState.mSource.getData().getLandTextures().getSize(); } void CSMTools::PopulateLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getLandTextures().getRecord(stage); if (!record.isDeleted()) { mState.mTarget->getData().getLandTextures().appendRecord(std::make_unique>( CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } CSMTools::MergeLandStage::MergeLandStage(MergeState& state) : mState(state) { } int CSMTools::MergeLandStage::setup() { return mState.mSource.getData().getLand().getSize(); } void CSMTools::MergeLandStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mState.mSource.getData().getLand().getRecord(stage); if (!record.isDeleted()) { mState.mTarget->getData().getLand().appendRecord(std::make_unique>( CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } } CSMTools::FixLandsAndLandTexturesMergeStage::FixLandsAndLandTexturesMergeStage(MergeState& state) : mState(state) { } int CSMTools::FixLandsAndLandTexturesMergeStage::setup() { // We will have no more than the source return mState.mSource.getData().getLand().getSize(); } void CSMTools::FixLandsAndLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { if (stage < mState.mTarget->getData().getLand().getSize()) { CSMWorld::IdTable& landTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_Lands)); CSMWorld::IdTable& ltexTable = dynamic_cast( *mState.mTarget->getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); const auto& id = mState.mTarget->getData().getLand().getId(stage); CSMWorld::TouchLandCommand cmd(landTable, ltexTable, id.getRefIdString()); cmd.redo(); // Get rid of base data const CSMWorld::Record& oldRecord = mState.mTarget->getData().getLand().getRecord(stage); 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) { } int CSMTools::CleanupLandTexturesMergeStage::setup() { return 1; } void CSMTools::CleanupLandTexturesMergeStage::perform(int stage, CSMDoc::Messages& messages) { auto& landTextures = mState.mTarget->getData().getLandTextures(); for (int i = 0; i < landTextures.getSize();) { if (!landTextures.getRecord(i).isModified()) landTextures.removeRows(i, 1); else ++i; } } openmw-openmw-0.49.0/apps/opencs/model/tools/mergestages.hpp000066400000000000000000000130351503074453300241330ustar00rootroot00000000000000#ifndef CSM_TOOLS_MERGESTAGES_H #define CSM_TOOLS_MERGESTAGES_H #include #include #include #include #include #include #include #include "../doc/stage.hpp" #include "../world/data.hpp" #include "mergestate.hpp" namespace CSMDoc { class Messages; } namespace CSMTools { class StartMergeStage : public CSMDoc::Stage { MergeState& mState; public: StartMergeStage(MergeState& state); 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. }; class FinishMergedDocumentStage : public CSMDoc::Stage { MergeState& mState; ToUTF8::Utf8Encoder mEncoder; public: FinishMergedDocumentStage(MergeState& state, ToUTF8::FromType encoding); 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. }; template > class MergeIdCollectionStage : public CSMDoc::Stage { MergeState& mState; Collection& (CSMWorld::Data::*mAccessor)(); public: MergeIdCollectionStage(MergeState& state, Collection& (CSMWorld::Data::*accessor)()); 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. }; template MergeIdCollectionStage::MergeIdCollectionStage( MergeState& state, Collection& (CSMWorld::Data::*accessor)()) : mState(state) , mAccessor(accessor) { } template int MergeIdCollectionStage::setup() { return (mState.mSource.getData().*mAccessor)().getSize(); } 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); if (!record.isDeleted()) target.appendRecord(std::make_unique>( CSMWorld::Record(CSMWorld::RecordBase::State_ModifiedOnly, nullptr, &record.get()))); } class MergeRefIdsStage : public CSMDoc::Stage { MergeState& mState; public: MergeRefIdsStage(MergeState& state); 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. }; class MergeReferencesStage : public CSMDoc::Stage { MergeState& mState; std::map mIndex; public: MergeReferencesStage(MergeState& state); 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. }; /// Adds all land texture records that could potentially be referenced when merging class PopulateLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: PopulateLandTexturesMergeStage(MergeState& state); 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. }; class MergeLandStage : public CSMDoc::Stage { MergeState& mState; public: MergeLandStage(MergeState& state); 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. }; /// During this stage, the complex process of combining LandTextures from /// potentially multiple plugins is undertaken. class FixLandsAndLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: FixLandsAndLandTexturesMergeStage(MergeState& state); 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. }; /// Removes base LandTexture records. This gets rid of the base records previously /// needed in FixLandsAndLandTexturesMergeStage. class CleanupLandTexturesMergeStage : public CSMDoc::Stage { MergeState& mState; public: CleanupLandTexturesMergeStage(MergeState& state); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/mergestate.hpp000066400000000000000000000010611503074453300237610ustar00rootroot00000000000000#ifndef CSM_TOOLS_MERGESTATE_H #define CSM_TOOLS_MERGESTATE_H #include #include #include #include "../doc/document.hpp" namespace CSMTools { struct MergeState { std::unique_ptr mTarget; CSMDoc::Document& mSource; bool mCompleted; std::map, int> mTextureIndices; // (texture, content file) -> new texture MergeState(CSMDoc::Document& source) : mSource(source) , mCompleted(false) { } }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/pathgridcheck.cpp000066400000000000000000000117251503074453300244240ustar00rootroot00000000000000#include "pathgridcheck.hpp" #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" #include "../world/idcollection.hpp" #include "../world/pathgrid.hpp" #include "../world/subcellcollection.hpp" #include "../world/universalid.hpp" CSMTools::PathgridCheckStage::PathgridCheckStage(const CSMWorld::SubCellCollection& pathgrids) : mPathgrids(pathgrids) { mIgnoreBaseRecords = false; } int CSMTools::PathgridCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mPathgrids.getSize(); } void CSMTools::PathgridCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mPathgrids.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const CSMWorld::Pathgrid& pathgrid = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Pathgrid, pathgrid.mId); // check the number of pathgrid points 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; for (const auto& edge : pathgrid.mEdges) { if (edge.mV0 < pathgrid.mPoints.size()) { auto& point = pointList[edge.mV0]; point.mConnectionNum++; // first check for duplicate edges size_t j = 0; for (; j < point.mOtherIndex.size(); ++j) { if (point.mOtherIndex[j] == edge.mV1) { std::ostringstream ss; 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 == point.mOtherIndex.size()) point.mOtherIndex.push_back(edge.mV1); } else { std::ostringstream ss; ss << "An edge is connected to a non-existent point #" << edge.mV0; messages.add(id, ss.str(), {}, CSMDoc::Message::Severity_Error); } } for (size_t i = 0; i < pathgrid.mPoints.size(); ++i) { // check that edges are bidirectional bool foundReverse = false; for (const auto& otherIndex : pointList[i].mOtherIndex) { for (const auto& other : pointList[otherIndex].mOtherIndex) { if (other == i) { foundReverse = true; break; } } if (!foundReverse) { std::ostringstream ss; 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 (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) { 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); duplList.push_back(i); break; } } } } // check pathgrid points that are not connected to anything 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); } } // TODO: check whether there are disconnected graphs } openmw-openmw-0.49.0/apps/opencs/model/tools/pathgridcheck.hpp000066400000000000000000000016521503074453300244270ustar00rootroot00000000000000#ifndef CSM_TOOLS_PATHGRIDCHECK_H #define CSM_TOOLS_PATHGRIDCHECK_H #include #include #include "../doc/stage.hpp" namespace CSMDoc { class Messages; } namespace CSMWorld { struct Pathgrid; template class SubCellCollection; } namespace CSMTools { struct Point { unsigned char mConnectionNum; std::vector mOtherIndex; Point() : mConnectionNum(0) , mOtherIndex(0) { } }; class PathgridCheckStage : public CSMDoc::Stage { const CSMWorld::SubCellCollection& mPathgrids; bool mIgnoreBaseRecords; public: explicit PathgridCheckStage(const CSMWorld::SubCellCollection& pathgrids); int setup() override; void perform(int stage, CSMDoc::Messages& messages) override; }; } #endif // CSM_TOOLS_PATHGRIDCHECK_H openmw-openmw-0.49.0/apps/opencs/model/tools/racecheck.cpp000066400000000000000000000054051503074453300235320ustar00rootroot00000000000000#include "racecheck.hpp" #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include #include #include #include #include #include void CSMTools::RaceCheckStage::performPerRecord(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRaces.getRecord(stage); if (record.isDeleted()) return; const ESM::Race& race = record.get(); // Consider mPlayable flag even when "Base" records are ignored if (race.mData.mFlags & 0x1) mPlayable = true; // Skip "Base" records (setting!) if (mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) return; 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); if (race.mDescription.empty()) messages.add(id, "Description is missing", "", CSMDoc::Message::Severity_Warning); // test for positive height if (race.mData.mMaleHeight <= 0) messages.add(id, "Male height is non-positive", "", CSMDoc::Message::Severity_Error); 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.mMaleWeight < 0) messages.add(id, "Male weight is negative", "", CSMDoc::Message::Severity_Error); 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) { 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) { mIgnoreBaseRecords = false; } int CSMTools::RaceCheckStage::setup() { mPlayable = false; mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mRaces.getSize() + 1; } void CSMTools::RaceCheckStage::perform(int stage, CSMDoc::Messages& messages) { if (stage == mRaces.getSize()) performFinal(messages); else performPerRecord(stage, messages); } openmw-openmw-0.49.0/apps/opencs/model/tools/racecheck.hpp000066400000000000000000000016351503074453300235400ustar00rootroot00000000000000#ifndef CSM_TOOLS_RACECHECK_H #define CSM_TOOLS_RACECHECK_H #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); void performFinal(CSMDoc::Messages& messages); public: RaceCheckStage(const CSMWorld::IdCollection& races); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/referenceablecheck.cpp000066400000000000000000001207321503074453300254030ustar00rootroot00000000000000#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 "../prefs/state.hpp" #include "../world/record.hpp" #include "../world/universalid.hpp" #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) { mIgnoreBaseRecords = false; } 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. const int bookSize(mReferencables.getBooks().getSize()); if (stage < bookSize) { bookCheck(stage, mReferencables.getBooks(), messages); return; } stage -= bookSize; const int activatorSize(mReferencables.getActivators().getSize()); if (stage < activatorSize) { activatorCheck(stage, mReferencables.getActivators(), messages); return; } stage -= activatorSize; const int potionSize(mReferencables.getPotions().getSize()); if (stage < potionSize) { potionCheck(stage, mReferencables.getPotions(), messages); return; } stage -= potionSize; const int apparatusSize(mReferencables.getApparati().getSize()); if (stage < apparatusSize) { apparatusCheck(stage, mReferencables.getApparati(), messages); return; } stage -= apparatusSize; const int armorSize(mReferencables.getArmors().getSize()); if (stage < armorSize) { armorCheck(stage, mReferencables.getArmors(), messages); return; } stage -= armorSize; const int clothingSize(mReferencables.getClothing().getSize()); if (stage < clothingSize) { clothingCheck(stage, mReferencables.getClothing(), messages); return; } stage -= clothingSize; const int containerSize(mReferencables.getContainers().getSize()); if (stage < containerSize) { containerCheck(stage, mReferencables.getContainers(), messages); return; } stage -= containerSize; const int doorSize(mReferencables.getDoors().getSize()); if (stage < doorSize) { doorCheck(stage, mReferencables.getDoors(), messages); return; } stage -= doorSize; const int ingredientSize(mReferencables.getIngredients().getSize()); if (stage < ingredientSize) { ingredientCheck(stage, mReferencables.getIngredients(), messages); return; } stage -= ingredientSize; const int creatureLevListSize(mReferencables.getCreatureLevelledLists().getSize()); if (stage < creatureLevListSize) { creaturesLevListCheck(stage, mReferencables.getCreatureLevelledLists(), messages); return; } stage -= creatureLevListSize; const int itemLevelledListSize(mReferencables.getItemLevelledList().getSize()); if (stage < itemLevelledListSize) { itemLevelledListCheck(stage, mReferencables.getItemLevelledList(), messages); return; } stage -= itemLevelledListSize; const int lightSize(mReferencables.getLights().getSize()); if (stage < lightSize) { lightCheck(stage, mReferencables.getLights(), messages); return; } stage -= lightSize; const int lockpickSize(mReferencables.getLocpicks().getSize()); if (stage < lockpickSize) { lockpickCheck(stage, mReferencables.getLocpicks(), messages); return; } stage -= lockpickSize; const int miscSize(mReferencables.getMiscellaneous().getSize()); if (stage < miscSize) { miscCheck(stage, mReferencables.getMiscellaneous(), messages); return; } stage -= miscSize; const int npcSize(mReferencables.getNPCs().getSize()); if (stage < npcSize) { npcCheck(stage, mReferencables.getNPCs(), messages); return; } stage -= npcSize; const int weaponSize(mReferencables.getWeapons().getSize()); if (stage < weaponSize) { weaponCheck(stage, mReferencables.getWeapons(), messages); return; } stage -= weaponSize; const int probeSize(mReferencables.getProbes().getSize()); if (stage < probeSize) { probeCheck(stage, mReferencables.getProbes(), messages); return; } stage -= probeSize; const int repairSize(mReferencables.getRepairs().getSize()); if (stage < repairSize) { repairCheck(stage, mReferencables.getRepairs(), messages); return; } stage -= repairSize; const int staticSize(mReferencables.getStatics().getSize()); if (stage < staticSize) { staticCheck(stage, mReferencables.getStatics(), messages); return; } stage -= staticSize; const int creatureSize(mReferencables.getCreatures().getSize()); if (stage < creatureSize) { creatureCheck(stage, mReferencables.getCreatures(), messages); return; } // if we come that far, we are about to perform our last, final check. finalCheck(messages); return; } int CSMTools::ReferenceableCheckStage::setup() { mPlayerPresent = false; mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mReferencables.getSize() + 1; } void CSMTools::ReferenceableCheckStage::bookCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Book& book = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Book, book.mId); inventoryItemCheck(book, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(book, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::activatorCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Activator& activator = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Activator, activator.mId); if (activator.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(activator.mModel) == -1) messages.add(id, "Model '" + activator.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(activator, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::potionCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Potion& potion = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Potion, potion.mId); inventoryItemCheck(potion, messages, id.toString()); 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& records, CSMDoc::Messages& messages) { 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::Apparatus& apparatus = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Apparatus, apparatus.mId); inventoryItemCheck(apparatus, messages, id.toString()); toolCheck(apparatus, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(apparatus, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::armorCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Armor& armor = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Armor, armor.mId); inventoryItemCheck(armor, messages, id.toString(), true); // Armor should have positive armor class, but 0 class is not an error if (armor.mData.mArmor < 0) messages.add(id, "Armor class is negative", "", CSMDoc::Message::Severity_Error); // Armor durability must be a positive number if (armor.mData.mHealth <= 0) messages.add(id, "Durability is non-positive", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(armor, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::clothingCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Clothing& clothing = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Clothing, clothing.mId); inventoryItemCheck(clothing, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(clothing, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::containerCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Container& container = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Container, container.mId); // checking for name if (container.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); // 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 messages.add(id, "Capacity is negative", "", CSMDoc::Message::Severity_Error); // 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& records, CSMDoc::Messages& messages) { 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::Creature& creature = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Creature, creature.mId); if (creature.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (creature.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(creature.mModel) == -1) messages.add(id, "Model '" + creature.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // stats checks if (creature.mData.mLevel <= 0) messages.add(id, "Level is non-positive", "", 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); if (creature.mData.mMagic < 0) messages.add(id, "Magic is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mStealth < 0) messages.add(id, "Stealth is negative", "", CSMDoc::Message::Severity_Warning); if (creature.mData.mHealth < 0) messages.add(id, "Health is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mMana < 0) messages.add(id, "Magicka is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mFatigue < 0) messages.add(id, "Fatigue is negative", "", CSMDoc::Message::Severity_Error); if (creature.mData.mSoul < 0) messages.add(id, "Soul value is negative", "", CSMDoc::Message::Severity_Error); if (creature.mAiData.mAlarm > 100) messages.add(id, "Alarm rating is over 100", "", CSMDoc::Message::Severity_Warning); if (creature.mAiData.mFight > 100) messages.add(id, "Fight rating is over 100", "", CSMDoc::Message::Severity_Warning); if (creature.mAiData.mFlee > 100) messages.add(id, "Flee rating is over 100", "", CSMDoc::Message::Severity_Warning); 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); } if (creature.mData.mGold < 0) messages.add(id, "Gold count is negative", "", CSMDoc::Message::Severity_Error); if (creature.mScale == 0) messages.add(id, "Scale is equal to zero", "", CSMDoc::Message::Severity_Error); if (!creature.mOriginal.empty()) { CSMWorld::RefIdData::LocalIndex index = mReferencables.searchId(creature.mOriginal); if (index.first == -1) 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.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& records, CSMDoc::Messages& messages) { 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::Door& door = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Door, door.mId); // usual, name or model if (door.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); if (door.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(door.mModel) == -1) messages.add(id, "Model '" + door.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); // Check that mentioned scripts exist scriptCheck(door, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::ingredientCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::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& records, CSMDoc::Messages& messages) { 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::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& records, CSMDoc::Messages& messages) { 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::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& records, CSMDoc::Messages& messages) { 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::Light& light = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Light, light.mId); if (light.mData.mRadius < 0) messages.add(id, "Light radius is negative", "", CSMDoc::Message::Severity_Error); if (light.mData.mFlags & ESM::Light::Carry) inventoryItemCheck(light, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(light, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::lockpickCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Lockpick& lockpick = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Lockpick, lockpick.mId); inventoryItemCheck(lockpick, messages, id.toString()); toolCheck(lockpick, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(lockpick, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::miscCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Miscellaneous& miscellaneous = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Miscellaneous, miscellaneous.mId); inventoryItemCheck(miscellaneous, messages, id.toString()); // Check that mentioned scripts exist scriptCheck(miscellaneous, messages, id.toString()); } 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); // Detect if player is present if (npc.mId == "Player") // Happy now, scrawl? mPlayerPresent = true; // Skip "Base" records (setting!) if (mIgnoreBaseRecords && baseRecord.mState == CSMWorld::RecordBase::State_BaseOnly) return; short level(npc.mNpdt.mLevel); int gold(npc.mNpdt.mGold); if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) // 12 = autocalculated { 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? return; } } if (level <= 0) messages.add(id, "Level is non-positive", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mAlarm > 100) messages.add(id, "Alarm rating is over 100", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mFight > 100) messages.add(id, "Fight rating is over 100", "", CSMDoc::Message::Severity_Warning); if (npc.mAiData.mFlee > 100) messages.add(id, "Flee rating is over 100", "", CSMDoc::Message::Severity_Warning); if (gold < 0) messages.add(id, "Gold count is negative", "", CSMDoc::Message::Severity_Error); if (npc.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); 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.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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (!npc.mFaction.empty() && mFactions.searchId(npc.mFaction) == -1) 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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); /// \todo Check gender, race and other body parts stuff validity for the specific NPC } if (npc.mHair.empty()) messages.add(id, "Hair is missing", "", CSMDoc::Message::Severity_Error); else { if (mBodyParts.searchId(npc.mHair) == -1) 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& records, CSMDoc::Messages& messages) { 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")) { 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.mSlash[0] > weapon.mData.mSlash[1]) messages.add(id, "Minimum slash damage higher than maximum", "", CSMDoc::Message::Severity_Warning); if (weapon.mData.mThrust[0] > weapon.mData.mThrust[1]) messages.add(id, "Minimum thrust damage higher than maximum", "", CSMDoc::Message::Severity_Warning); } 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)) { // checking of health if (weapon.mData.mHealth == 0) messages.add(id, "Durability is equal to zero", "", CSMDoc::Message::Severity_Warning); if (weapon.mData.mReach < 0) messages.add(id, "Reach is negative", "", CSMDoc::Message::Severity_Error); } } // Check that mentioned scripts exist scriptCheck(weapon, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::probeCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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::Probe& probe = (dynamic_cast&>(baseRecord)).get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Probe, probe.mId); inventoryItemCheck(probe, messages, id.toString()); toolCheck(probe, messages, id.toString(), true); // Check that mentioned scripts exist scriptCheck(probe, messages, id.toString()); } void CSMTools::ReferenceableCheckStage::repairCheck( int stage, const CSMWorld::RefIdDataContainer& records, CSMDoc::Messages& messages) { 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); 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& records, CSMDoc::Messages& messages) { 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); if (staticElement.mModel.empty()) messages.add(id, "Model is missing", "", CSMDoc::Message::Severity_Error); else if (mModels.searchId(staticElement.mModel) == -1) messages.add(id, "Model '" + staticElement.mModel + "' does not exist", "", CSMDoc::Message::Severity_Error); } // final check void CSMTools::ReferenceableCheckStage::finalCheck(CSMDoc::Messages& messages) { if (!mPlayerPresent) 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) { for (size_t i = 0; i < itemList.size(); ++i) { 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); else { // 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); } } } } // Templates begins here 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 if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); // Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); // 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 if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) { std::string ddsIcon = someItem.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(someID, "Icon '" + someItem.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } if (enchantable && someItem.mData.mEnchant < 0) messages.add(someID, "Enchantment points number is negative", "", CSMDoc::Message::Severity_Error); } 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 if (someItem.mData.mWeight < 0) messages.add(someID, "Weight is negative", "", CSMDoc::Message::Severity_Error); // Checking for value if (someItem.mData.mValue < 0) messages.add(someID, "Value is negative", "", CSMDoc::Message::Severity_Error); // 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 if (someItem.mIcon.empty()) messages.add(someID, "Icon is missing", "", CSMDoc::Message::Severity_Error); else if (mIcons.searchId(someItem.mIcon) == -1) { std::string ddsIcon = someItem.mIcon; if (!(Misc::ResourceHelpers::changeExtensionToDds(ddsIcon) && mIcons.searchId(ddsIcon) != -1)) messages.add(someID, "Icon '" + someItem.mIcon + "' does not exist", "", CSMDoc::Message::Severity_Error); } } 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) messages.add(someID, "Number of uses is non-positive", "", CSMDoc::Message::Severity_Error); } 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( 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); } for (const auto& element : someList.mList) { if (mReferencables.searchId(element.mId).first == -1) messages.add(someID, "Object '" + element.mId.getRefIdString() + "' does not exist", "", 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( 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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/referenceablecheck.hpp000066400000000000000000000132121503074453300254020ustar00rootroot00000000000000#ifndef REFERENCEABLECHECKSTAGE_H #define REFERENCEABLECHECKSTAGE_H #include #include #include "../doc/stage.hpp" #include "../world/idcollection.hpp" #include "../world/refiddata.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& 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 openmw-openmw-0.49.0/apps/opencs/model/tools/referencecheck.cpp000066400000000000000000000123701503074453300245550ustar00rootroot00000000000000#include "referencecheck.hpp" #include #include "../prefs/state.hpp" #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) { const CSMWorld::Record& record = mReferences.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; 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 { // Check for non existing referenced object 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); bool isLight = localIndex.second == CSMWorld::UniversalId::Type_Light; if ((isLight && cellRef.mChargeFloat < -1) || (!isLight && cellRef.mChargeInt < -1)) messages.add(id, "Invalid charge", "", CSMDoc::Message::Severity_Error); } } // 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.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.getRefIdString() + "' does not exist", "", CSMDoc::Message::Severity_Error); if (cellRef.mFaction.empty()) { if (cellRef.mFactionRank != -2) messages.add(id, "Reference without a faction has a faction rank", "", CSMDoc::Message::Severity_Error); } else { if (mFactions.searchId(cellRef.mFaction) == -1) 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(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); // Check if enchantement points aren't negative or are at full (-1) if (cellRef.mEnchantmentCharge < -1) messages.add(id, "Negative number of enchantment points", "", 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(); } openmw-openmw-0.49.0/apps/opencs/model/tools/referencecheck.hpp000066400000000000000000000025321503074453300245610ustar00rootroot00000000000000#ifndef CSM_TOOLS_REFERENCECHECK_H #define CSM_TOOLS_REFERENCECHECK_H #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, 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; }; } #endif // CSM_TOOLS_REFERENCECHECK_H openmw-openmw-0.49.0/apps/opencs/model/tools/regioncheck.cpp000066400000000000000000000037631503074453300241100ustar00rootroot00000000000000#include "regioncheck.hpp" #include #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include #include #include #include #include #include CSMTools::RegionCheckStage::RegionCheckStage(const CSMWorld::IdCollection& regions) : mRegions(regions) { mIgnoreBaseRecords = false; } int CSMTools::RegionCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mRegions.getSize(); } void CSMTools::RegionCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mRegions.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Region& region = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Region, region.mId); // test for empty name if (region.mName.empty()) messages.add(id, "Name is missing", "", CSMDoc::Message::Severity_Error); /// \todo test that the ID in mSleeplist exists // test that chances add up to 100 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.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 } openmw-openmw-0.49.0/apps/opencs/model/tools/regioncheck.hpp000066400000000000000000000014321503074453300241040ustar00rootroot00000000000000#ifndef CSM_TOOLS_REGIONCHECK_H #define CSM_TOOLS_REGIONCHECK_H #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: RegionCheckStage(const CSMWorld::IdCollection& regions); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/reportmodel.cpp000066400000000000000000000121321503074453300241510ustar00rootroot00000000000000#include "reportmodel.hpp" #include #include #include #include #include #include #include "../world/columns.hpp" CSMTools::ReportModel::ReportModel(bool fieldColumn, bool severityColumn) : mColumnField(-1) , mColumnSeverity(-1) { int index = 3; if (severityColumn) mColumnSeverity = index++; if (fieldColumn) mColumnField = index++; mColumnDescription = index; } int CSMTools::ReportModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return static_cast(mRows.size()); } int CSMTools::ReportModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return mColumnDescription + 1; } QVariant CSMTools::ReportModel::data(const QModelIndex& index, int role) const { 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()); else return static_cast(mRows.at(index.row()).mId.getType()); case Column_Id: { 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()); } if (index.column() == mColumnDescription) return QString::fromUtf8(mRows.at(index.row()).mMessage.c_str()); if (index.column() == mColumnField) { std::string field; std::istringstream stream(mRows.at(index.row()).mHint); char type, ignore; int fieldIndex; if ((stream >> type >> ignore >> fieldIndex) && (type == 'r' || type == 'R')) { field = CSMWorld::Columns::getName(static_cast(fieldIndex)); } return QString::fromUtf8(field.c_str()); } if (index.column() == mColumnSeverity) { 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 { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Vertical) return QVariant(); switch (section) { case Column_Type: return "Type"; case Column_Id: return "ID"; } if (section == mColumnDescription) return "Description"; if (section == mColumnField) return "Field"; if (section == mColumnSeverity) return "Severity"; return "-"; } bool CSMTools::ReportModel::removeRows(int row, int count, const QModelIndex& parent) { if (parent.isValid()) return false; if (count > 0) { beginRemoveRows(parent, row, row + count - 1); mRows.erase(mRows.begin() + row, mRows.begin() + row + count); endRemoveRows(); } return true; } void CSMTools::ReportModel::add(const CSMDoc::Message& message) { beginInsertRows(QModelIndex(), static_cast(mRows.size()), static_cast(mRows.size())); mRows.push_back(message); endInsertRows(); } void CSMTools::ReportModel::flagAsReplaced(int 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"); hint[0] = 'r'; line.mHint = std::move(hint); emit dataChanged(this->index(index, 0), this->index(index, columnCount())); } const CSMWorld::UniversalId& CSMTools::ReportModel::getUniversalId(int row) const { return mRows.at(row).mId; } std::string CSMTools::ReportModel::getHint(int row) const { return mRows.at(row).mHint; } void CSMTools::ReportModel::clear() { if (!mRows.empty()) { beginRemoveRows(QModelIndex(), 0, static_cast(mRows.size()) - 1); mRows.clear(); endRemoveRows(); } } 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) ++count; return count; } openmw-openmw-0.49.0/apps/opencs/model/tools/reportmodel.hpp000066400000000000000000000030551503074453300241620ustar00rootroot00000000000000#ifndef CSM_TOOLS_REPORTMODEL_H #define CSM_TOOLS_REPORTMODEL_H #include #include #include #include #include #include "../doc/messages.hpp" namespace CSMWorld { class UniversalId; } namespace CSMTools { class ReportModel : public QAbstractTableModel { Q_OBJECT std::vector mRows; // Fixed columns enum Columns { Column_Type = 0, Column_Id = 1, Column_Hint = 2 }; // Configurable columns int mColumnDescription; int mColumnField; int mColumnSeverity; public: ReportModel(bool fieldColumn = false, bool severityColumn = true); int rowCount(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 headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; void add(const CSMDoc::Message& message); void flagAsReplaced(int index); const CSMWorld::UniversalId& getUniversalId(int row) const; std::string getHint(int row) const; void clear(); // Return number of messages with Error or SeriousError severity. int countErrors() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/scriptcheck.cpp000066400000000000000000000103641503074453300241240ustar00rootroot00000000000000#include "scriptcheck.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../doc/document.hpp" #include "../world/data.hpp" #include "../prefs/state.hpp" CSMDoc::Message::Severity CSMTools::ScriptCheckStage::getSeverity(Type type) { switch (type) { 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) { std::ostringstream stream; CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); stream << message << " (" << loc.mLiteral << ")" << " @ line " << loc.mLine + 1 << ", column " << loc.mColumn; std::ostringstream hintStream; hintStream << "l:" << loc.mLine + 1 << " " << loc.mColumn; mMessages->add(id, stream.str(), hintStream.str(), getSeverity(type)); } void CSMTools::ScriptCheckStage::report(const std::string& message, Type type) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << message; mMessages->add(id, stream.str(), "", getSeverity(type)); } 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); Compiler::registerExtensions(mExtensions); mContext.setExtensions(&mExtensions); mIgnoreBaseRecords = false; } int CSMTools::ScriptCheckStage::setup() { std::string warnings = CSMPrefs::get()["Scripts"]["warnings"].toString(); if (warnings == "Ignore") mWarningMode = Mode_Ignore; else if (warnings == "Normal") mWarningMode = Mode_Normal; else if (warnings == "Strict") mWarningMode = Mode_Strict; mContext.clear(); mMessages = nullptr; mId = ESM::RefId(); Compiler::ErrorHandler::reset(); mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mDocument.getData().getScripts().getSize(); } void CSMTools::ScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mDocument.getData().getScripts().getRecord(stage); mId = mDocument.getData().getScripts().getId(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; mMessages = &messages; switch (mWarningMode) { case Mode_Ignore: setWarningsMode(0); break; case Mode_Normal: setWarningsMode(1); break; case Mode_Strict: setWarningsMode(2); break; } try { mFile = record.get().mId.getRefIdString(); std::istringstream input(record.get().mScriptText); Compiler::Scanner scanner(*this, input, mContext.getExtensions()); Compiler::FileParser parser(*this, mContext); scanner.scan(parser); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Script, mId); std::ostringstream stream; stream << error.what(); messages.add(id, stream.str(), "", CSMDoc::Message::Severity_SeriousError); } mMessages = nullptr; } openmw-openmw-0.49.0/apps/opencs/model/tools/scriptcheck.hpp000066400000000000000000000030431503074453300241250ustar00rootroot00000000000000#ifndef CSM_TOOLS_SCRIPTCHECK_H #define CSM_TOOLS_SCRIPTCHECK_H #include #include #include #include #include "../doc/stage.hpp" #include "../world/scriptcontext.hpp" 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; ESM::RefId mId; std::string mFile; CSMDoc::Messages* mMessages; WarningMode mWarningMode; bool mIgnoreBaseRecords; CSMDoc::Message::Severity getSeverity(Type type); void report(const std::string& message, const Compiler::TokenLoc& loc, Type type) override; ///< Report error to the user. void report(const std::string& message, Type type) override; ///< Report a file related error public: ScriptCheckStage(const CSMDoc::Document& document); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/search.cpp000066400000000000000000000217471503074453300230760ustar00rootroot00000000000000#include "search.hpp" #include #include #include #include #include #include #include #include #include #include "../doc/document.hpp" #include "../doc/messages.hpp" #include "../world/columnbase.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 { // using QString here for easier handling of case folding. 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) { 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()); pos += search.length(); } } void CSMTools::Search::searchRegExCell(const CSMWorld::IdTableBase* model, const QModelIndex& index, const CSMWorld::UniversalId& id, bool writable, CSMDoc::Messages& messages) const { // TODO: verify regular expression before starting a search if (!mRegExp.isValid()) return; QString text = model->data(index).toString(); QRegularExpressionMatchIterator i = mRegExp.globalMatch(text); while (i.hasNext()) { 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()); } } 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(); if (data == mValue) { 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); } } QString CSMTools::Search::formatDescription(const QString& description, int pos, int length) const { 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)); // compensate for Windows nonsense text.remove('\r'); // join text = before + "" + highlight + "" + after; // improve layout for single line display text.replace("\n", "<CR>"); text.replace('\t', ' '); return text; } QString CSMTools::Search::flatten(const QString& text) const { QString flat(text); 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(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)"); } 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.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) { if (type != Type_RecordState) throw std::logic_error("invalid search parameter (int)"); } void CSMTools::Search::configure(const CSMWorld::IdTableBase* model) { mColumns.clear(); int columns = model->columnCount(); for (int i = 0; i < columns; ++i) { CSMWorld::ColumnBase::Display display = static_cast( model->headerData(i, Qt::Horizontal, static_cast(CSMWorld::ColumnBase::Role_Display)).toInt()); bool consider = false; switch (mType) { case Type_Text: case Type_TextRegEx: 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)) { consider = true; } break; case Type_RecordState: if (display == CSMWorld::ColumnBase::Display_RecordState) consider = true; break; case Type_None: break; } if (consider) mColumns.insert(i); } 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 { for (std::set::const_iterator iter(mColumns.begin()); iter != mColumns.end(); ++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()); bool writable = model->flags(index) & Qt::ItemIsEditable; switch (mType) { case Type_Text: case Type_Id: searchTextCell(model, index, id, writable, messages); break; case Type_TextRegEx: case Type_IdRegEx: searchRegExCell(model, index, id, writable, messages); break; case Type_RecordState: searchRecordStateCell(model, index, id, writable, messages); break; case Type_None: break; } } } 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 { 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); 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()))); } } 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); int row = model->getModelIndex(id.getId(), model->findColumnIndex(CSMWorld::Columns::ColumnId_Id)).row(); searchRow(model, row, messages); for (CSMDoc::Messages::Iterator iter(messages.begin()); iter != messages.end(); ++iter) if (iter->mHint == messageHint) return true; return false; } openmw-openmw-0.49.0/apps/opencs/model/tools/search.hpp000066400000000000000000000053021503074453300230700ustar00rootroot00000000000000#ifndef CSM_TOOLS_SEARCH_H #define CSM_TOOLS_SEARCH_H #include #include #include #include class QModelIndex; namespace CSMDoc { class Messages; class Document; } namespace CSMWorld { class IdTableBase; class UniversalId; } 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; 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) #endif openmw-openmw-0.49.0/apps/opencs/model/tools/searchoperation.cpp000066400000000000000000000024611503074453300250070ustar00rootroot00000000000000#include "searchoperation.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) { 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)))); setDefaultSeverity(CSMDoc::Message::Severity_Info); } void CSMTools::SearchOperation::configure(const Search& search) { mSearch = search; } void CSMTools::SearchOperation::appendStage(SearchStage* stage) { CSMDoc::Operation::appendStage(stage); stage->setOperation(this); } const CSMTools::Search& CSMTools::SearchOperation::getSearch() const { return mSearch; } openmw-openmw-0.49.0/apps/opencs/model/tools/searchoperation.hpp000066400000000000000000000013571503074453300250170ustar00rootroot00000000000000#ifndef CSM_TOOLS_SEARCHOPERATION_H #define CSM_TOOLS_SEARCHOPERATION_H #include "../doc/operation.hpp" #include "search.hpp" namespace CSMDoc { class Document; } namespace CSMTools { class SearchStage; class SearchOperation : public CSMDoc::Operation { Search mSearch; public: SearchOperation(CSMDoc::Document& document); /// \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. const Search& getSearch() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/searchstage.cpp000066400000000000000000000013061503074453300241070ustar00rootroot00000000000000#include "searchstage.hpp" #include "../world/idtablebase.hpp" #include #include "searchoperation.hpp" 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); return mModel->rowCount(); } void CSMTools::SearchStage::perform(int stage, CSMDoc::Messages& messages) { mSearch.searchRow(mModel, stage, messages); } void CSMTools::SearchStage::setOperation(const SearchOperation* operation) { mOperation = operation; } openmw-openmw-0.49.0/apps/opencs/model/tools/searchstage.hpp000066400000000000000000000014231503074453300241140ustar00rootroot00000000000000#ifndef CSM_TOOLS_SEARCHSTAGE_H #define CSM_TOOLS_SEARCHSTAGE_H #include "../doc/stage.hpp" #include "search.hpp" namespace CSMDoc { class Messages; } namespace CSMWorld { class IdTableBase; } namespace CSMTools { class SearchOperation; class SearchStage : public CSMDoc::Stage { const CSMWorld::IdTableBase* mModel; Search mSearch; const SearchOperation* mOperation; public: SearchStage(const CSMWorld::IdTableBase* model); 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 setOperation(const SearchOperation* operation); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/skillcheck.cpp000066400000000000000000000027521503074453300237400ustar00rootroot00000000000000#include "skillcheck.hpp" #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include #include #include #include #include #include CSMTools::SkillCheckStage::SkillCheckStage(const CSMWorld::IdCollection& skills) : mSkills(skills) { mIgnoreBaseRecords = false; } int CSMTools::SkillCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSkills.getSize(); } void CSMTools::SkillCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSkills.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Skill& skill = record.get(); 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) { messages.add(id, "Use value #" + std::to_string(i) + " is negative", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/skillcheck.hpp000066400000000000000000000014201503074453300237340ustar00rootroot00000000000000#ifndef CSM_TOOLS_SKILLCHECK_H #define CSM_TOOLS_SKILLCHECK_H #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: SkillCheckStage(const CSMWorld::IdCollection& skills); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/soundcheck.cpp000066400000000000000000000033741503074453300237530ustar00rootroot00000000000000#include "soundcheck.hpp" #include #include "../prefs/state.hpp" #include "../world/universalid.hpp" #include #include #include #include #include #include #include CSMTools::SoundCheckStage::SoundCheckStage( const CSMWorld::IdCollection& sounds, const CSMWorld::Resources& soundfiles) : mSounds(sounds) , mSoundFiles(soundfiles) { mIgnoreBaseRecords = false; } int CSMTools::SoundCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSounds.getSize(); } void CSMTools::SoundCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSounds.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Sound& sound = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Sound, sound.mId); if (sound.mData.mMinRange > sound.mData.mMaxRange) { messages.add(id, "Minimum range is larger than maximum range", "", CSMDoc::Message::Severity_Warning); } if (sound.mSound.empty()) { messages.add(id, "Sound file is missing", "", CSMDoc::Message::Severity_Error); } else if (mSoundFiles.searchId(sound.mSound) == -1) { messages.add(id, "Sound file '" + sound.mSound + "' does not exist", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/soundcheck.hpp000066400000000000000000000016241503074453300237540ustar00rootroot00000000000000#ifndef CSM_TOOLS_SOUNDCHECK_H #define CSM_TOOLS_SOUNDCHECK_H #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: SoundCheckStage(const CSMWorld::IdCollection& sounds, const CSMWorld::Resources& soundfiles); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/soundgencheck.cpp000066400000000000000000000046431503074453300244450ustar00rootroot00000000000000#include "soundgencheck.hpp" #include #include "../prefs/state.hpp" #include "../world/refiddata.hpp" #include "../world/universalid.hpp" #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; } int CSMTools::SoundGenCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSoundGens.getSize(); } void CSMTools::SoundGenCheckStage::perform(int stage, CSMDoc::Messages& messages) { 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; const ESM::SoundGenerator& soundGen = record.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_SoundGen, soundGen.mId); if (!soundGen.mCreature.empty()) { CSMWorld::RefIdData::LocalIndex creatureIndex = mObjects.getDataSet().searchId(soundGen.mCreature); if (creatureIndex.first == -1) { 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.getRefIdString() + "' is not a creature", "", CSMDoc::Message::Severity_Error); } } if (soundGen.mSound.empty()) { messages.add(id, "Sound is missing", "", CSMDoc::Message::Severity_Error); } else if (mSounds.searchId(soundGen.mSound) == -1) { messages.add( id, "Sound '" + soundGen.mSound.getRefIdString() + "' doesn't exist", "", CSMDoc::Message::Severity_Error); } } openmw-openmw-0.49.0/apps/opencs/model/tools/soundgencheck.hpp000066400000000000000000000021411503074453300244410ustar00rootroot00000000000000#ifndef CSM_TOOLS_SOUNDGENCHECK_HPP #define CSM_TOOLS_SOUNDGENCHECK_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; public: SoundGenCheckStage(const CSMWorld::IdCollection& soundGens, const CSMWorld::IdCollection& sounds, const CSMWorld::RefIdCollection& objects); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/spellcheck.cpp000066400000000000000000000030451503074453300237350ustar00rootroot00000000000000#include "spellcheck.hpp" #include #include #include #include #include #include #include #include #include "../prefs/state.hpp" #include "effectlistcheck.hpp" CSMTools::SpellCheckStage::SpellCheckStage(const CSMWorld::IdCollection& spells) : mSpells(spells) { mIgnoreBaseRecords = false; } int CSMTools::SpellCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mSpells.getSize(); } void CSMTools::SpellCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& record = mSpells.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && record.mState == CSMWorld::RecordBase::State_BaseOnly) || record.isDeleted()) return; const ESM::Spell& spell = record.get(); 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) messages.add(id, "Spell cost is negative", "", CSMDoc::Message::Severity_Error); effectListCheck(spell.mEffects.mList, messages, id); } openmw-openmw-0.49.0/apps/opencs/model/tools/spellcheck.hpp000066400000000000000000000014201503074453300237350ustar00rootroot00000000000000#ifndef CSM_TOOLS_SPELLCHECK_H #define CSM_TOOLS_SPELLCHECK_H #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: SpellCheckStage(const CSMWorld::IdCollection& spells); 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/startscriptcheck.cpp000066400000000000000000000031151503074453300251760ustar00rootroot00000000000000#include "startscriptcheck.hpp" #include #include "../prefs/state.hpp" #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) { mIgnoreBaseRecords = false; } void CSMTools::StartScriptCheckStage::perform(int stage, CSMDoc::Messages& messages) { 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; const auto& scriptId = record.get().mId; CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_StartScript, scriptId); if (mScripts.searchId(scriptId) == -1) messages.add( id, "Start script " + scriptId.getRefIdString() + " does not exist", "", CSMDoc::Message::Severity_Error); } int CSMTools::StartScriptCheckStage::setup() { mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mStartScripts.getSize(); } openmw-openmw-0.49.0/apps/opencs/model/tools/startscriptcheck.hpp000066400000000000000000000014651503074453300252110ustar00rootroot00000000000000#ifndef CSM_TOOLS_STARTSCRIPTCHECK_H #define CSM_TOOLS_STARTSCRIPTCHECK_H #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: StartScriptCheckStage(const CSMWorld::IdCollection& startScripts, const CSMWorld::IdCollection& scripts); void perform(int stage, CSMDoc::Messages& messages) override; int setup() override; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/tools.cpp000066400000000000000000000231671503074453300227670ustar00rootroot00000000000000#include "tools.hpp" #include #include #include #include #include #include "../doc/document.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 "referenceablecheck.hpp" #include "referencecheck.hpp" #include "regioncheck.hpp" #include "reportmodel.hpp" #include "scriptcheck.hpp" #include "searchoperation.hpp" #include "skillcheck.hpp" #include "soundcheck.hpp" #include "soundgencheck.hpp" #include "spellcheck.hpp" #include "startscriptcheck.hpp" #include "topicinfocheck.hpp" #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; } return nullptr; } const CSMDoc::OperationHolder* CSMTools::Tools::get(int type) const { return const_cast(this)->get(type); } CSMDoc::OperationHolder* CSMTools::Tools::getVerifier() { if (!mVerifierOperation) { mVerifierOperation = new CSMDoc::Operation(CSMDoc::State_Verifying, false); 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 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), mandatoryRefIds)); mVerifierOperation->appendStage(new SkillCheckStage(mData.getSkills())); mVerifierOperation->appendStage(new ClassCheckStage(mData.getClasses())); mVerifierOperation->appendStage(new FactionCheckStage(mData.getFactions())); mVerifierOperation->appendStage(new RaceCheckStage(mData.getRaces())); 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 SpellCheckStage(mData.getSpells())); 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 ReferenceCheckStage(mData.getReferences(), mData.getReferenceables(), mData.getCells(), mData.getFactions(), mData.getBodyParts())); mVerifierOperation->appendStage(new ScriptCheckStage(mDocument)); mVerifierOperation->appendStage(new StartScriptCheckStage(mData.getStartScripts(), mData.getScripts())); 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); } 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) { // index 0: load error log mReports.insert(std::make_pair(mNextReportNumber++, new ReportModel)); mActiveReports.insert(std::make_pair(CSMDoc::State_Loading, 0)); 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, &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 } CSMTools::Tools::~Tools() { if (mVerifierOperation) { mVerifier.abortAndWait(); delete mVerifierOperation; } if (mSearchOperation) { mSearch.abortAndWait(); delete mSearchOperation; } if (mMergeOperation) { mMerge.abortAndWait(); delete mMergeOperation; } for (std::map::iterator iter(mReports.begin()); iter != mReports.end(); ++iter) delete iter->second; } CSMWorld::UniversalId CSMTools::Tools::runVerifier(const CSMWorld::UniversalId& reportId) { 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)); mActiveReports[CSMDoc::State_Verifying] = reportNumber; getVerifier()->start(); return CSMWorld::UniversalId(CSMWorld::UniversalId::Type_VerificationResults, reportNumber); } CSMWorld::UniversalId CSMTools::Tools::newSearch() { mReports.insert(std::make_pair(mNextReportNumber++, new ReportModel(true, false))); return CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Search, mNextReportNumber - 1); } 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->configure(search); mSearch.start(); } 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, &MergeOperation::mergeDone, this, &Tools::mergeDone); } target->flagAsDirty(); mMergeOperation->setTarget(std::move(target)); mMerge.start(); } void CSMTools::Tools::abortOperation(int 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, }; int result = 0; 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) { 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()); } void CSMTools::Tools::verifierMessage(const CSMDoc::Message& message, int type) { std::map::iterator iter = mActiveReports.find(type); if (iter != mActiveReports.end()) mReports[iter->second]->add(message); } openmw-openmw-0.49.0/apps/opencs/model/tools/tools.hpp000066400000000000000000000054261503074453300227720ustar00rootroot00000000000000#ifndef CSM_TOOLS_TOOLS_H #define CSM_TOOLS_TOOLS_H #include #include #include #include #include #include "../doc/operationholder.hpp" namespace CSMWorld { class Data; } namespace CSMDoc { class Operation; class Document; struct Message; } namespace CSMTools { class ReportModel; class Search; class SearchOperation; class MergeOperation; 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; // not implemented Tools(const Tools&); Tools& operator=(const Tools&); CSMDoc::OperationHolder* getVerifier(); CSMDoc::OperationHolder* get(int type); ///< Returns a 0-pointer, if operation hasn't been used yet. const CSMDoc::OperationHolder* get(int type) const; ///< Returns a 0-pointer, if operation hasn't been used yet. public: Tools(CSMDoc::Document& document, ToUTF8::FromType encoding); 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()); /// Return ID of the report for this search. CSMWorld::UniversalId newSearch(); void runSearch(const CSMWorld::UniversalId& searchId, const Search& search); void runMerge(std::unique_ptr target); void abortOperation(int type); ///< \attention The operation is not aborted immediately. int getRunningOperations() const; ReportModel* getReport(const CSMWorld::UniversalId& id); ///< The ownership of the returned report is not transferred. private slots: void verifierMessage(const CSMDoc::Message& message, int type); signals: void progress(int current, int max, int type); 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/tools/topicinfocheck.cpp000066400000000000000000000331741503074453300246160ustar00rootroot00000000000000#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, 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) { mIgnoreBaseRecords = false; } int CSMTools::TopicInfoCheckStage::setup() { // Generate list of cell names for reference checking mCellNames.clear(); for (int i = 0; i < mCells.getSize(); ++i) { const CSMWorld::Record& cellRecord = mCells.getRecord(i); if (cellRecord.isDeleted()) continue; mCellNames.insert(cellRecord.get().mName); } // Cell names can also include region names for (int i = 0; i < mRegions.getSize(); ++i) { const CSMWorld::Record& regionRecord = mRegions.getRecord(i); if (regionRecord.isDeleted()) continue; mCellNames.insert(regionRecord.get().mName); } // Default cell name const int index = mGameSettings.searchId(ESM::RefId::stringRefId("sDefaultCellname")); if (index != -1) { const CSMWorld::Record& gmstRecord = mGameSettings.getRecord(index); if (!gmstRecord.isDeleted() && gmstRecord.get().mValue.getType() == ESM::VT_String) { mCellNames.insert(gmstRecord.get().mValue.getString()); } } mIgnoreBaseRecords = CSMPrefs::get()["Reports"]["ignore-base-records"].isTrue(); return mTopicInfos.getSize(); } void CSMTools::TopicInfoCheckStage::perform(int stage, CSMDoc::Messages& messages) { const CSMWorld::Record& infoRecord = mTopicInfos.getRecord(stage); // Skip "Base" records (setting!) and "Deleted" records if ((mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) || infoRecord.isDeleted()) return; const CSMWorld::Info& topicInfo = infoRecord.get(); // There should always be a topic that matches int topicIndex = mTopics.searchId(topicInfo.mTopicId); const CSMWorld::Record& topicRecord = mTopics.getRecord(topicIndex); if (topicRecord.isDeleted()) return; const ESM::Dialogue& topic = topicRecord.get(); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_TopicInfo, topicInfo.mId); // Check fields if (!topicInfo.mActor.empty()) { verifyActor(topicInfo.mActor, id, messages); } if (!topicInfo.mClass.empty()) { verifyId(topicInfo.mClass, mClasses, id, messages); } if (!topicInfo.mCell.empty()) { verifyCell(topicInfo.mCell.getRefIdString(), id, messages); } if (!topicInfo.mFaction.empty() && !topicInfo.mFactionLess) { if (verifyId(topicInfo.mFaction, mFactions, id, messages)) { verifyFactionRank(topicInfo.mFaction, topicInfo.mData.mRank, id, messages); } } if (!topicInfo.mPcFaction.empty()) { if (verifyId(topicInfo.mPcFaction, mFactions, id, messages)) { verifyFactionRank(topicInfo.mPcFaction, topicInfo.mData.mPCrank, id, messages); } } if (topicInfo.mData.mGender < -1 || topicInfo.mData.mGender > 1) { messages.add(id, "Gender is invalid", "", CSMDoc::Message::Severity_Error); } if (!topicInfo.mRace.empty()) { verifyId(topicInfo.mRace, mRaces, id, messages); } if (!topicInfo.mSound.empty()) { verifySound(topicInfo.mSound, id, messages); } if (topicInfo.mResponse.empty() && topic.mType != ESM::Dialogue::Voice) { messages.add(id, "Response is empty", "", CSMDoc::Message::Severity_Warning); } // Check info conditions for (const auto& select : topicInfo.mSelects) { verifySelectStruct(select, id, messages); } } // Verification functions 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 '" + actorString + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { 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)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifyCell( const std::string& cell, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mCellNames.find(cell) == mCellNames.end()) { messages.add(id, "Cell '" + cell + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifyFactionRank( const ESM::RefId& factionName, int rank, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (rank < -1) { std::ostringstream stream; stream << "Faction rank is set to " << rank << ", but it should be set to -1 if there are no rank requirements"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Warning); return false; } int index = mFactions.searchId(factionName); const ESM::Faction& faction = mFactions.getRecord(index).get(); int limit = 0; for (; limit < 10; ++limit) { if (faction.mRanks[limit].empty()) break; } if (rank >= limit) { std::ostringstream stream; stream << "Faction rank is set to " << rank << " which is more than the maximum of " << limit - 1 << " for the '" << factionName << "' faction"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } return true; } 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 '" + idString + "' does not exist"), "", CSMDoc::Message::Severity_Error); return false; } else if (mReferencables.getRecord(index).isDeleted()) { messages.add(id, ("Deleted item '" + idString + "' is being referenced"), "", CSMDoc::Message::Severity_Error); return false; } else { switch (index.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: { 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)"; messages.add(id, stream.str(), "", CSMDoc::Message::Severity_Error); return false; } } } return true; } bool CSMTools::TopicInfoCheckStage::verifySelectStruct( const ESM::DialogueCondition& select, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { CSMWorld::ConstInfoSelectWrapper infoCondition(select); if (select.mFunction == ESM::DialogueCondition::Function_None) { messages.add(id, "Invalid condition '" + infoCondition.toString() + "'", "", CSMDoc::Message::Severity_Error); return false; } else if (infoCondition.conditionIsAlwaysTrue()) { 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); return false; } // Id checks if (select.mFunction == ESM::DialogueCondition::Function_Global && !verifyId(ESM::RefId::stringRefId(select.mVariable), mGlobals, id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_Journal && !verifyId(ESM::RefId::stringRefId(select.mVariable), mJournals, id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_Item && !verifyItem(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_Dead && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_NotId && !verifyActor(ESM::RefId::stringRefId(select.mVariable), id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_NotFaction && !verifyId(ESM::RefId::stringRefId(select.mVariable), mFactions, id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_NotClass && !verifyId(ESM::RefId::stringRefId(select.mVariable), mClasses, id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_NotRace && !verifyId(ESM::RefId::stringRefId(select.mVariable), mRaces, id, messages)) { return false; } else if (select.mFunction == ESM::DialogueCondition::Function_NotCell && !verifyCell(select.mVariable, id, messages)) { return false; } return true; } bool CSMTools::TopicInfoCheckStage::verifySound( const std::string& sound, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages) { if (mSoundFiles.searchId(sound) == -1) { messages.add(id, "Sound file '" + sound + "' does not exist", "", CSMDoc::Message::Severity_Error); return false; } return true; } template 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, 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 " + std::string(T::getRecordType()) + " record '" + name.getRefIdString() + "' is being referenced", "", CSMDoc::Message::Severity_Error); return false; } return true; } openmw-openmw-0.49.0/apps/opencs/model/tools/topicinfocheck.hpp000066400000000000000000000066711503074453300246250ustar00rootroot00000000000000#ifndef CSM_TOOLS_TOPICINFOCHECK_HPP #define CSM_TOOLS_TOPICINFOCHECK_HPP #include #include #include #include #include #include "../world/idcollection.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, const CSMWorld::Resources& soundFiles); int setup() override; ///< \return number of steps void perform(int step, CSMDoc::Messages& messages) override; ///< Messages resulting from this stage will be appended to \a messages private: const CSMWorld::InfoCollection& mTopicInfos; const CSMWorld::IdCollection& mCells; const CSMWorld::IdCollection& mClasses; const CSMWorld::IdCollection& mFactions; const CSMWorld::IdCollection& mGameSettings; const CSMWorld::IdCollection& mGlobals; const CSMWorld::IdCollection& mJournals; const CSMWorld::IdCollection& mRaces; const CSMWorld::IdCollection& mRegions; const CSMWorld::IdCollection& mTopics; const CSMWorld::RefIdData& mReferencables; const CSMWorld::Resources& mSoundFiles; std::set mCellNames; bool mIgnoreBaseRecords; // These return false when not successful and write an error 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 ESM::RefId& name, const CSMWorld::IdCollection& collection, const CSMWorld::UniversalId& id, CSMDoc::Messages& messages); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/000077500000000000000000000000001503074453300211015ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/model/world/actoradapter.cpp000066400000000000000000000535341503074453300242700ustar00rootroot00000000000000#include "actoradapter.hpp" #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 ESM::RefId& ActorAdapter::RaceData::getId() const { return mId; } bool ActorAdapter::RaceData::isBeast() const { return mIsBeast; } ActorAdapter::RaceData::RaceData() { mIsBeast = false; } bool ActorAdapter::RaceData::handlesPart(ESM::PartReferenceType type) const { switch (type) { case ESM::PRT_Skirt: case ESM::PRT_Shield: case ESM::PRT_RPauldron: case ESM::PRT_LPauldron: case ESM::PRT_Weapon: return false; default: return true; } } const ESM::RefId& ActorAdapter::RaceData::getFemalePart(ESM::PartReferenceType index) const { return mFemaleParts[ESM::getMeshPart(index)]; } const ESM::RefId& ActorAdapter::RaceData::getMalePart(ESM::PartReferenceType index) const { return mMaleParts[ESM::getMeshPart(index)]; } 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 ESM::RefId& partId) { mFemaleParts[index] = partId; addOtherDependency(partId); } void ActorAdapter::RaceData::setMalePart(ESM::BodyPart::MeshPart index, const ESM::RefId& partId) { mMaleParts[index] = partId; addOtherDependency(partId); } void ActorAdapter::RaceData::addOtherDependency(const ESM::RefId& id) { if (!id.empty()) mDependencies.emplace(id); } 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 = ESM::RefId(); for (auto& str : mMaleParts) str = ESM::RefId(); mDependencies.clear(); // Mark self as a dependency addOtherDependency(id); } ActorAdapter::ActorData::ActorData() { mCreature = false; mFemale = false; } const ESM::RefId& ActorAdapter::ActorData::getId() const { return mId; } bool ActorAdapter::ActorData::isCreature() const { return mCreature; } bool ActorAdapter::ActorData::isFemale() const { return mFemale; } std::string ActorAdapter::ActorData::getSkeleton() const { if (mCreature || !mSkeletonOverride.empty()) return "meshes\\" + mSkeletonOverride; bool beast = mRaceData ? mRaceData->isBeast() : false; if (beast) return CSMPrefs::get()["Models"]["baseanimkna"].toString(); else if (mFemale) return CSMPrefs::get()["Models"]["baseanimfemale"].toString(); else return CSMPrefs::get()["Models"]["baseanim"].toString(); } ESM::RefId ActorAdapter::ActorData::getPart(ESM::PartReferenceType index) const { auto it = mParts.find(index); if (it == mParts.end()) { if (mRaceData && mRaceData->handlesPart(index)) { if (mFemale) { // Note: we should use male parts for females as fallback if (const ESM::RefId femalePart = mRaceData->getFemalePart(index); !femalePart.empty()) return femalePart; } return mRaceData->getMalePart(index); } return {}; } return it->second.first; } 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 ESM::RefId& partId, int priority) { auto it = mParts.find(index); if (it != mParts.end()) { if (it->second.second >= priority) return; } mParts[index] = std::make_pair(partId, priority); addOtherDependency(partId); } void ActorAdapter::ActorData::addOtherDependency(const ESM::RefId& id) { if (!id.empty()) mDependencies.emplace(id); } void ActorAdapter::ActorData::reset_data( const ESM::RefId& id, const std::string& skeleton, bool isCreature, bool isFemale, RaceDataPtr raceData) { mId = id; mCreature = isCreature; mFemale = isFemale; mSkeletonOverride = skeleton; mRaceData = raceData; mParts.clear(); mDependencies.clear(); // Mark self and race as a dependency addOtherDependency(id); if (raceData) addOtherDependency(raceData->getId()); } ActorAdapter::ActorAdapter(Data& data) : mReferenceables(data.getReferenceables()) , mRaces(data.getRaces()) , mBodyParts(data.getBodyParts()) { // Setup qt slots and signals QAbstractItemModel* refModel = data.getTableModel(UniversalId::Type_Referenceable); 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, &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, &QAbstractItemModel::rowsInserted, this, &ActorAdapter::handleBodyPartsInserted); connect(partModel, &QAbstractItemModel::dataChanged, this, &ActorAdapter::handleBodyPartChanged); connect( partModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ActorAdapter::handleBodyPartsAboutToBeRemoved); } ActorAdapter::ActorDataPtr ActorAdapter::getActorData(const ESM::RefId& id) { // Return cached actor data if it exists ActorDataPtr data = mCachedActors.get(id); if (data) { return data; } // Create the actor data data = std::make_shared(); setupActor(id, data); mCachedActors.insert(id, data); return data; } void ActorAdapter::handleReferenceablesInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top level are pertinent. Others are caught by dataChanged handler. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } } // Update affected updateDirty(); } void ActorAdapter::handleReferenceableChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; // Handle each record for (int row = start; row <= end; ++row) { auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } // Update affected updateDirty(); } void ActorAdapter::handleReferenceablesAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only rows at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { auto refId = mReferenceables.getId(row); markDirtyDependency(refId); } } } void ActorAdapter::handleReferenceablesRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleReferenceablesAboutToBeRemoved updateDirty(); } void ActorAdapter::handleRacesInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { auto raceId = mReferenceables.getId(row); markDirtyDependency(raceId); } } // Update affected updateDirty(); } void ActorAdapter::handleRaceChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; for (int row = start; row <= end; ++row) { auto raceId = mRaces.getId(row); markDirtyDependency(raceId); } // Update affected updateDirty(); } void ActorAdapter::handleRacesAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only changes at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { auto raceId = mRaces.getId(row); markDirtyDependency(raceId); } } } void ActorAdapter::handleRacesRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleRacesAboutToBeRemoved updateDirty(); } void ActorAdapter::handleBodyPartsInserted(const QModelIndex& parent, int start, int end) { // Only rows added at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { // Race specified by part may need update auto& record = mBodyParts.getRecord(row); if (!record.isDeleted()) { markDirtyDependency(record.get().mRace); } auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } } // Update affected updateDirty(); } void ActorAdapter::handleBodyPartChanged(const QModelIndex& topLeft, const QModelIndex& botRight) { int start = getHighestIndex(topLeft).row(); int end = getHighestIndex(botRight).row(); // A change to record status (ex. Deleted) returns an invalid botRight if (end == -1) end = start; for (int row = start; row <= end; ++row) { // Race specified by part may need update auto& record = mBodyParts.getRecord(row); if (!record.isDeleted()) { markDirtyDependency(record.get().mRace); } // Update entries with a tracked dependency auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } // Update affected updateDirty(); } void ActorAdapter::handleBodyPartsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { // Only changes at the top are pertinent. if (!parent.isValid()) { for (int row = start; row <= end; ++row) { auto partId = mBodyParts.getId(row); markDirtyDependency(partId); } } } void ActorAdapter::handleBodyPartsRemoved(const QModelIndex& parent, int start, int end) { // Changes specified in handleBodyPartsAboutToBeRemoved updateDirty(); } QModelIndex ActorAdapter::getHighestIndex(QModelIndex index) const { while (index.parent().isValid()) index = index.parent(); return index; } ActorAdapter::RaceDataPtr ActorAdapter::getRaceData(const ESM::RefId& id) { // Return cached race data if it exists RaceDataPtr data = mCachedRaces.get(id); if (data) return data; // Create the race data data = std::make_shared(); setupRace(id, data); mCachedRaces.insert(id, data); return data; } void ActorAdapter::setupActor(const ESM::RefId& id, ActorDataPtr data) { int index = mReferenceables.searchId(id); if (index == -1) { // Record does not exist data->reset_data(id); emit actorChanged(id); return; } auto& record = mReferenceables.getRecord(index); if (record.isDeleted()) { // Record is deleted and therefore not accessible data->reset_data(id); emit actorChanged(id); return; } const int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType); int type = mReferenceables.getData(index, TypeColumn).toInt(); if (type == UniversalId::Type_Creature) { // Valid creature record setupCreature(id, std::move(data)); emit actorChanged(id); } else if (type == UniversalId::Type_Npc) { // Valid npc record setupNpc(id, std::move(data)); emit actorChanged(id); } else { // Wrong record type data->reset_data(id); emit actorChanged(id); } } void ActorAdapter::setupRace(const ESM::RefId& id, RaceDataPtr data) { int index = mRaces.searchId(id); if (index == -1) { // Record does not exist data->reset_data(id); return; } auto& raceRecord = mRaces.getRecord(index); if (raceRecord.isDeleted()) { // Record is deleted, so not accessible data->reset_data(id); return; } auto& race = raceRecord.get(); 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) { auto& partRecord = mBodyParts.getRecord(i); if (partRecord.isDeleted()) { // Record is deleted, so not accessible. continue; } auto& part = partRecord.get(); if (part.mRace == id && part.mData.mType == ESM::BodyPart::MT_Skin && !ESM::isFirstPersonBodyPart(part)) { 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); } } } 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(), std::move(raceData)); // Add head and hair data->setPart(ESM::PRT_Head, npc.mHead, 0); data->setPart(ESM::PRT_Hair, npc.mHair, 0); // Add inventory items for (auto& item : npc.mInventory.mList) { if (item.mCount <= 0) continue; auto itemId = item.mItem; addNpcItem(itemId, data); } } void ActorAdapter::addNpcItem(const ESM::RefId& itemId, ActorDataPtr data) { int index = mReferenceables.searchId(itemId); if (index == -1) { // Item does not exist yet data->addOtherDependency(itemId); return; } auto& record = mReferenceables.getRecord(index); if (record.isDeleted()) { // Item cannot be accessed yet data->addOtherDependency(itemId); return; } // Convenience function to add a parts list to actor data auto addParts = [&](const std::vector& list, int priority) { for (auto& part : list) { ESM::RefId partId; auto partType = (ESM::PartReferenceType)part.mPart; if (data->isFemale()) partId = part.mFemale; if (partId.empty()) partId = part.mMale; data->setPart(partType, partId, priority); // An another vanilla quirk: hide hairs if an item replaces Head part if (partType == ESM::PRT_Head) data->setPart(ESM::PRT_Hair, ESM::RefId(), priority); } }; int TypeColumn = mReferenceables.findColumnIndex(Columns::ColumnId_RecordType); int type = mReferenceables.getData(index, TypeColumn).toInt(); if (type == UniversalId::Type_Armor) { auto& armor = dynamic_cast&>(record).get(); addParts(armor.mParts.mParts, 1); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); } else if (type == UniversalId::Type_Clothing) { auto& clothing = dynamic_cast&>(record).get(); 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 }; } else if (clothing.mData.mType == ESM::Clothing::Skirt) { parts = { ESM::PRT_Groin, ESM::PRT_RLeg, ESM::PRT_LLeg }; } std::vector reservedList; for (const auto& p : parts) { ESM::PartReference pr; pr.mPart = p; reservedList.emplace_back(pr); } int priority = parts.size(); addParts(clothing.mParts.mParts, priority); addParts(reservedList, priority); // Changing parts could affect what is picked for rendering data->addOtherDependency(itemId); } } void ActorAdapter::setupCreature(const ESM::RefId& id, ActorDataPtr data) { // Record is known to exist and is not deleted int index = mReferenceables.searchId(id); auto& creature = dynamic_cast&>(mReferenceables.getRecord(index)).get(); data->reset_data(id, creature.mModel, true); } void ActorAdapter::markDirtyDependency(const ESM::RefId& dep) { for (auto raceIt : mCachedRaces) { if (raceIt->hasDependency(dep)) mDirtyRaces.emplace(raceIt->getId()); } for (auto actorIt : mCachedActors) { if (actorIt->hasDependency(dep)) mDirtyActors.emplace(actorIt->getId()); } } void ActorAdapter::updateDirty() { // Handle races before actors, since actors are dependent on race for (auto& race : mDirtyRaces) { RaceDataPtr data = mCachedRaces.get(race); if (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. for (auto actorIt : mCachedActors) { if (actorIt->hasDependency(race)) mDirtyActors.emplace(actorIt->getId()); } } } mDirtyRaces.clear(); for (auto& actor : mDirtyActors) { ActorDataPtr data = mCachedActors.get(actor); if (data) { setupActor(actor, std::move(data)); } } mDirtyActors.clear(); } } openmw-openmw-0.49.0/apps/opencs/model/world/actoradapter.hpp000066400000000000000000000155771503074453300243020ustar00rootroot00000000000000#ifndef CSM_WOLRD_ACTORADAPTER_H #define CSM_WOLRD_ACTORADAPTER_H #include #include #include #include #include #include #include #include #include #include #include #include #include "idcollection.hpp" namespace ESM { struct Race; } namespace CSMWorld { class Data; class RefIdCollection; /// Adapts multiple collections to provide the data needed to render /// an npc or creature. class ActorAdapter : public QObject { Q_OBJECT public: /// A list indexed by ESM::PartReferenceType using ActorPartList = std::map>; /// A list indexed by ESM::BodyPart::MeshPart using RacePartList = std::array; /// Tracks unique strings using RefIdSet = std::unordered_set; struct WeightsHeights { osg::Vec2f mMaleWeightHeight; osg::Vec2f mFemaleWeightHeight; }; /// Contains base race data shared between actors class RaceData { public: RaceData(); /// Retrieves the id of the race represented 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 ESM::RefId& getFemalePart(ESM::PartReferenceType index) const; /// Retrieves the associated body part 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 ESM::RefId& id) const; /// Sets the associated part if it's empty and marks a dependency 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 ESM::RefId& partId); /// Marks an additional dependency void addOtherDependency(const ESM::RefId& id); /// Clears parts and dependencies 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; ESM::RefId mId; bool mIsBeast; RacePartList mFemaleParts; RacePartList mMaleParts; WeightsHeights mWeightsHeights; RefIdSet mDependencies; }; using RaceDataPtr = std::shared_ptr; /// Contains all the data needed to render an actor. Tracks dependencies /// so that pertinent data changes can be checked. class ActorData { public: ActorData(); /// Retrieves the id of the actor represented const ESM::RefId& getId() const; /// Checks if the actor is a creature bool isCreature() const; /// Checks if the actor is female bool isFemale() const; /// Returns the skeleton the actor should use for attaching parts to std::string getSkeleton() const; /// Retrieves the associated actor part ESM::RefId getPart(ESM::PartReferenceType index) const; const osg::Vec2f& getRaceWeightHeight() const; /// Checks if the actor has a data dependency bool hasDependency(const ESM::RefId& id) const; /// Sets the actor part used and marks a dependency void setPart(ESM::PartReferenceType partIndex, const ESM::RefId& partId, int priority); /// Marks an additional dependency for the actor void addOtherDependency(const ESM::RefId& id); /// Clears race, parts, and dependencies void reset_data(const ESM::RefId& actorId, const std::string& skeleton = "", bool isCreature = false, bool female = true, RaceDataPtr raceData = nullptr); private: ESM::RefId mId; bool mCreature; bool mFemale; std::string mSkeletonOverride; RaceDataPtr mRaceData; ActorPartList mParts; RefIdSet mDependencies; }; using ActorDataPtr = std::shared_ptr; ActorAdapter(Data& data); /// Obtains the shared data for a given actor ActorDataPtr getActorData(const ESM::RefId& refId); signals: void actorChanged(const ESM::RefId& refId); public slots: void handleReferenceablesInserted(const QModelIndex&, int, int); void handleReferenceableChanged(const QModelIndex&, const QModelIndex&); void handleReferenceablesAboutToBeRemoved(const QModelIndex&, int, int); void handleReferenceablesRemoved(const QModelIndex&, int, int); void handleRacesInserted(const QModelIndex&, int, int); void handleRaceChanged(const QModelIndex&, const QModelIndex&); void handleRacesAboutToBeRemoved(const QModelIndex&, int, int); void handleRacesRemoved(const QModelIndex&, int, int); void handleBodyPartsInserted(const QModelIndex&, int, int); void handleBodyPartChanged(const QModelIndex&, const QModelIndex&); void handleBodyPartsAboutToBeRemoved(const QModelIndex&, int, int); void handleBodyPartsRemoved(const QModelIndex&, int, int); private: ActorAdapter(const ActorAdapter&) = delete; ActorAdapter& operator=(const ActorAdapter&) = delete; QModelIndex getHighestIndex(QModelIndex) const; RaceDataPtr getRaceData(const ESM::RefId& raceId); void setupActor(const ESM::RefId& id, ActorDataPtr data); void setupRace(const ESM::RefId& id, RaceDataPtr data); void setupNpc(const ESM::RefId& id, ActorDataPtr data); void addNpcItem(const ESM::RefId& itemId, ActorDataPtr data); void setupCreature(const ESM::RefId& id, ActorDataPtr data); 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 RefIdSet mDirtyActors; // Actors that need updating RefIdSet mDirtyRaces; // Races that need updating }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/cell.cpp000066400000000000000000000003301503074453300225200ustar00rootroot00000000000000#include "cell.hpp" #include void CSMWorld::Cell::load(ESM::ESMReader& esm, bool& isDeleted) { ESM::Cell::load(esm, isDeleted, false); mId = ESM::RefId::stringRefId(ESM::Cell::mId.toString()); } openmw-openmw-0.49.0/apps/opencs/model/world/cell.hpp000066400000000000000000000007771503074453300225440ustar00rootroot00000000000000#ifndef CSM_WOLRD_CELL_H #define CSM_WOLRD_CELL_H #include #include namespace ESM { class ESMReader; } namespace CSMWorld { /// \brief Wrapper for Cell record /// /// \attention The mData.mX and mData.mY fields of the ESM::Cell struct are not used. /// Exterior cell coordinates are encoded in the cell ID. struct Cell : public ESM::Cell { ESM::RefId mId; void load(ESM::ESMReader& esm, bool& isDeleted); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/cellcoordinates.cpp000066400000000000000000000117701503074453300247650ustar00rootroot00000000000000#include "cellcoordinates.hpp" #include #include #include #include #include #include #include CSMWorld::CellCoordinates::CellCoordinates() : mX(0) , mY(0) { } CSMWorld::CellCoordinates::CellCoordinates(int x, int y) : mX(x) , mY(y) { } CSMWorld::CellCoordinates::CellCoordinates(const std::pair& coordinates) : mX(coordinates.first) , mY(coordinates.second) { } int CSMWorld::CellCoordinates::getX() const { return mX; } int CSMWorld::CellCoordinates::getY() const { return mY; } CSMWorld::CellCoordinates CSMWorld::CellCoordinates::move(int x, int y) const { return CellCoordinates(mX + x, mY + y); } 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 cellId = "#" + std::to_string(x) + " " + std::to_string(y); return cellId; } bool CSMWorld::CellCoordinates::isExteriorCell(const std::string& id) { return (!id.empty() && id[0] == '#'); } bool CSMWorld::CellCoordinates::isExteriorCell(const ESM::RefId& id) { return id.startsWith("#"); } std::pair CSMWorld::CellCoordinates::fromId(const std::string& id) { // no worldspace for now, needs to be changed for 1.1 if (isExteriorCell(id)) { int x, y; char ignore; std::istringstream stream(id); if (stream >> ignore >> x >> y) return std::make_pair(CellCoordinates(x, y), true); } return std::make_pair(CellCoordinates(), false); } std::pair CSMWorld::CellCoordinates::coordinatesToCellIndex(float x, float y) { return std::make_pair(std::floor(x / Constants::CellSizeInUnits), std::floor(y / Constants::CellSizeInUnits)); } std::pair CSMWorld::CellCoordinates::toTextureCoords(const osg::Vec3d& worldPos) { const auto xd = static_cast(worldPos.x() * ESM::Land::LAND_TEXTURE_SIZE / ESM::Land::REAL_SIZE - 0.25f); const auto yd = static_cast(worldPos.y() * ESM::Land::LAND_TEXTURE_SIZE / ESM::Land::REAL_SIZE + 0.25f); const auto x = static_cast(std::floor(xd)); const auto y = static_cast(std::floor(yd)); return std::make_pair(x, y); } std::pair CSMWorld::CellCoordinates::toVertexCoords(const osg::Vec3d& worldPos) { const auto xd = static_cast(worldPos.x() * (ESM::Land::LAND_SIZE - 1) / ESM::Land::REAL_SIZE + 0.5f); const auto yd = static_cast(worldPos.y() * (ESM::Land::LAND_SIZE - 1) / ESM::Land::REAL_SIZE + 0.5f); const auto x = static_cast(std::floor(xd)); const auto y = static_cast(std::floor(yd)); return std::make_pair(x, y); } float CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(int textureGlobal) { return ESM::Land::REAL_SIZE * (static_cast(textureGlobal) + 0.25f) / ESM::Land::LAND_TEXTURE_SIZE; } float CSMWorld::CellCoordinates::textureGlobalYToWorldCoords(int textureGlobal) { return ESM::Land::REAL_SIZE * (static_cast(textureGlobal) - 0.25f) / ESM::Land::LAND_TEXTURE_SIZE; } float CSMWorld::CellCoordinates::vertexGlobalToWorldCoords(int vertexGlobal) { return ESM::Land::REAL_SIZE * static_cast(vertexGlobal) / (ESM::Land::LAND_SIZE - 1); } 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)); } std::string CSMWorld::CellCoordinates::textureGlobalToCellId(const std::pair& textureGlobal) { int x = std::floor(static_cast(textureGlobal.first) / ESM::Land::LAND_TEXTURE_SIZE); int y = std::floor(static_cast(textureGlobal.second) / ESM::Land::LAND_TEXTURE_SIZE); return generateId(x, y); } std::string CSMWorld::CellCoordinates::vertexGlobalToCellId(const std::pair& vertexGlobal) { int x = std::floor(static_cast(vertexGlobal.first) / (ESM::Land::LAND_SIZE - 1)); int y = std::floor(static_cast(vertexGlobal.second) / (ESM::Land::LAND_SIZE - 1)); return generateId(x, y); } bool CSMWorld::operator==(const CellCoordinates& left, const CellCoordinates& right) { return left.getX() == right.getX() && left.getY() == right.getY(); } bool CSMWorld::operator!=(const CellCoordinates& left, const CellCoordinates& right) { return !(left == right); } bool CSMWorld::operator<(const CellCoordinates& left, const CellCoordinates& right) { if (left.getX() < right.getX()) return true; if (left.getX() > right.getX()) return false; return left.getY() < right.getY(); } std::ostream& CSMWorld::operator<<(std::ostream& stream, const CellCoordinates& coordiantes) { return stream << coordiantes.getX() << ", " << coordiantes.getY(); } openmw-openmw-0.49.0/apps/opencs/model/world/cellcoordinates.hpp000066400000000000000000000057731503074453300250000ustar00rootroot00000000000000#ifndef CSM_WOLRD_CELLCOORDINATES_H #define CSM_WOLRD_CELLCOORDINATES_H #include #include #include #include namespace osg { class Vec3d; } namespace ESM { class RefId; } namespace CSMWorld { class CellCoordinates { int mX; int mY; public: CellCoordinates(); CellCoordinates(int x, int y); CellCoordinates(const std::pair& coordinates); int getX() const; int getY() const; 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); 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 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 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 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 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 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 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); std::ostream& operator<<(std::ostream& stream, const CellCoordinates& coordiantes); } Q_DECLARE_METATYPE(CSMWorld::CellCoordinates) #endif openmw-openmw-0.49.0/apps/opencs/model/world/cellselection.cpp000066400000000000000000000034141503074453300244340ustar00rootroot00000000000000#include "cellselection.hpp" #include "cellcoordinates.hpp" #include #include #include #include CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::begin() const { return mCells.begin(); } CSMWorld::CellSelection::Iterator CSMWorld::CellSelection::end() const { return mCells.end(); } bool CSMWorld::CellSelection::add(const CellCoordinates& coordinates) { return mCells.insert(coordinates).second; } void CSMWorld::CellSelection::remove(const CellCoordinates& coordinates) { mCells.erase(coordinates); } bool CSMWorld::CellSelection::has(const CellCoordinates& coordinates) const { return mCells.find(coordinates) != end(); } int CSMWorld::CellSelection::getSize() const { return mCells.size(); } CSMWorld::CellCoordinates CSMWorld::CellSelection::getCentre() const { if (mCells.empty()) throw std::logic_error("call of getCentre on empty cell selection"); double x = 0; double y = 0; for (Iterator iter = begin(); iter != end(); ++iter) { x += iter->getX(); y += iter->getY(); } x /= mCells.size(); y /= mCells.size(); Iterator closest = begin(); double distance = std::numeric_limits::max(); 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); if (delta < distance) { distance = delta; closest = iter; } } return *closest; } void CSMWorld::CellSelection::move(int x, int y) { Container moved; for (Iterator iter = begin(); iter != end(); ++iter) moved.insert(iter->move(x, y)); mCells.swap(moved); } openmw-openmw-0.49.0/apps/opencs/model/world/cellselection.hpp000066400000000000000000000027371503074453300244500ustar00rootroot00000000000000#ifndef CSM_WOLRD_CELLSELECTION_H #define CSM_WOLRD_CELLSELECTION_H #include #include #include namespace CSMWorld { /// \brief Selection of cells in a paged worldspace /// /// \note The CellSelection does not specify the worldspace it applies to. class CellSelection { public: typedef std::set Container; typedef Container::const_iterator Iterator; private: Container mCells; public: Iterator begin() const; Iterator end() const; 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? void remove(const CellCoordinates& coordinates); ///< ignored if the cell specified by \a coordinates is not part of the selection. 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); }; } Q_DECLARE_METATYPE(CSMWorld::CellSelection) #endif openmw-openmw-0.49.0/apps/opencs/model/world/collection.hpp000066400000000000000000000465501503074453300237570ustar00rootroot00000000000000#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 "collectionbase.hpp" #include "columnbase.hpp" #include "columnimp.hpp" #include "info.hpp" #include "land.hpp" #include "record.hpp" #include "ref.hpp" namespace CSMWorld { inline std::pair parseInfoRefId(const ESM::RefId& infoId) { 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; } 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 auto getRecordId(const T& record) { return record.mId; } inline void setRecordId(const ESM::RefId& id, Land& record) { int x = 0; int y = 0; Land::parseUniqueRecordId(id.getRefIdString(), x, y); record.mX = x; record.mY = y; } inline ESM::RefId getRecordId(const Land& record) { return ESM::RefId::stringRefId(Land::createUniqueRecordId(record.mX, record.mY)); } inline ESM::RefId getRecordId(const ESM::MagicEffect& record) { return ESM::RefId::stringRefId(CSMWorld::getStringId(record.mId)); } inline void setRecordId(const ESM::RefId& id, ESM::MagicEffect& record) { int index = ESM::MagicEffect::indexNameToIndex(id.getRefIdString()); record.mId = ESM::RefId::index(ESM::REC_MGEF, static_cast(index)); } inline void setRecordId(const ESM::RefId& id, ESM::Skill& record) { if (const auto* skillId = id.getIf()) record.mId = *skillId; throw std::runtime_error("Invalid skill id: " + id.toDebugString()); } /// \brief Single-type record collection template class Collection : public CollectionBase { public: typedef ESXRecordT ESXRecord; private: std::vector>> mRecords; std::map mIndex; std::vector*> mColumns; protected: const std::vector>>& getRecords() const; void reorderRowsImp(const std::vector& indexOrder); 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? int cloneRecordImp(const ESM::RefId& origin, const ESM::RefId& dest, UniversalId::Type type); ///< Returns the index of the clone. int touchRecordImp(const ESM::RefId& id); ///< Returns the index of the record on success, -1 on failure. public: Collection() = default; Collection(const Collection&) = delete; Collection& operator=(const Collection&) = delete; ~Collection() override; void add(const ESXRecordT& record); ///< Add a new record (modified) int getSize() const override; ESM::RefId getId(int index) const override; int getIndex(const ESM::RefId& id) const override; int getColumns() const override; QVariant getData(int index, int column) const override; void setData(int index, int column, const QVariant& data) override; const ColumnBase& getColumn(int column) const override; void merge(); ///< Merge modified into base. void purge(); ///< Remove records that are flagged as erased. void removeRows(int index, int count) 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 void cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; 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. 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, 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(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 const Record& getRecord(const ESM::RefId& id) const override; const Record& getRecord(int index) const override; 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 std::vector getIds(bool listDeleted = true) const override; ///< Return a sorted collection of all IDs /// /// \param listDeleted include deleted record in the list 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. 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, std::unique_ptr> record); ///< \attention This function must not change the ID. NestableColumn* getNestableColumn(int column) const; }; template const std::vector>>& Collection::getRecords() const { return mRecords; } template void Collection::reorderRowsImp(const std::vector& indexOrder) { 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) { if (!newOrder.empty()) { int size = static_cast(newOrder.size()); // check that all indices are present std::vector test(newOrder); std::sort(test.begin(), test.end()); if (*test.begin() != 0 || *--test.end() != size - 1) return false; // reorder records std::vector>> buffer(size); for (int i = 0; i < size; ++i) { buffer[newOrder[i]] = std::move(mRecords[baseIndex + i]); if (buffer[newOrder[i]]) buffer[newOrder[i]]->setModified(buffer[newOrder[i]]->get()); } std::move(buffer.begin(), buffer.end(), mRecords.begin() + baseIndex); // adjust index for (auto& [id, index] : mIndex) if (index >= baseIndex && index < baseIndex + size) index = newOrder.at(index - baseIndex) + baseIndex; } return true; } template int Collection::cloneRecordImp( const ESM::RefId& origin, const ESM::RefId& destination, UniversalId::Type type) { auto copy = std::make_unique>(); copy->mModified = getRecord(origin).get(); copy->mState = RecordBase::State_ModifiedOnly; setRecordId(destination, copy->get()); if constexpr (std::is_same_v) { if (type == UniversalId::Type_Reference) { CSMWorld::CellRef* ptr = (CSMWorld::CellRef*)©->mModified; ptr->mRefNum.mIndex = 0; } } 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 ESM::RefId& id) { const int index = getIndex(id); Record& record = *mRecords.at(index); if (record.isDeleted()) throw std::runtime_error("attempt to touch deleted record from collection of " + std::string(ESXRecordT::getRecordType()) + ": " + id.toDebugString()); if (!record.isModified()) { record.setModified(record.get()); return index; } return -1; } 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 ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { const int index = cloneRecordImp(origin, destination, type); mRecords.at(index)->get().setPlugin(-1); } template bool Collection::touchRecord(const ESM::RefId& id) { return touchRecordImp(id) != -1; } template <> inline bool Collection::touchRecord(const ESM::RefId& id) { const int index = touchRecordImp(id); if (index >= 0) { mRecords.at(index)->get().setPlugin(-1); return true; } return false; } template Collection::~Collection() { for (typename std::vector*>::iterator iter(mColumns.begin()); iter != mColumns.end(); ++iter) delete *iter; } template void Collection::add(const ESXRecordT& record) { const ESM::RefId id = getRecordId(record); auto iter = mIndex.find(id); if (iter == mIndex.end()) { auto record2 = std::make_unique>(); record2->mState = Record::State_ModifiedOnly; record2->mModified = record; insertRecord(std::move(record2), getAppendIndex(id)); } else { mRecords[iter->second]->setModified(record); } } template int Collection::getSize() const { return mRecords.size(); } template ESM::RefId Collection::getId(int index) const { return getRecordId(mRecords.at(index)->get()); } template int Collection::getIndex(const ESM::RefId& id) const { int index = searchId(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 { return mColumns.size(); } template QVariant Collection::getData(int index, int column) const { return mColumns.at(column)->get(*mRecords.at(index)); } template void Collection::setData(int index, int column, const QVariant& data) { return mColumns.at(column)->set(*mRecords.at(index), data); } template const ColumnBase& Collection::getColumn(int column) const { return *mColumns.at(column); } template NestableColumn* Collection::getNestableColumn(int column) const { if (column < 0 || column >= static_cast(mColumns.size())) throw std::runtime_error( "column index out of range [0, " + std::to_string(mColumns.size()) + "): " + std::to_string(column)); return mColumns.at(column); } template void Collection::addColumn(Column* column) { mColumns.push_back(column); } template void Collection::merge() { for (typename std::vector>>::iterator iter(mRecords.begin()); iter != mRecords.end(); ++iter) (*iter)->merge(); purge(); } template void Collection::purge() { int i = 0; while (i < static_cast(mRecords.size())) { if (mRecords[i]->isErased()) removeRows(i, 1); else ++i; } } template void Collection::removeRows(int index, int count) { mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count); auto iter = mIndex.begin(); while (iter != mIndex.end()) { if (iter->second >= index) { if (iter->second >= index + count) { iter->second -= count; ++iter; } else { iter = mIndex.erase(iter); } } else ++iter; } } template void Collection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) { ESXRecordT record; setRecordId(id, record); record.blank(); 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(std::move(record2), getAppendIndex(id, type), type); } template int Collection::searchId(const ESM::RefId& id) const { const auto iter = mIndex.find(id); if (iter == mIndex.end()) return -1; return iter->second; } template void Collection::replace(int index, std::unique_ptr record) { std::unique_ptr> tmp(static_cast*>(record.release())); mRecords.at(index) = std::move(tmp); } template void Collection::appendRecord(std::unique_ptr record, UniversalId::Type type) { int index = getAppendIndex(getRecordId(static_cast*>(record.get())->get()), type); insertRecord(std::move(record), index, type); } template int Collection::getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const { return static_cast(mRecords.size()); } template std::vector Collection::getIds(bool listDeleted) const { std::vector ids; for (auto iter = mIndex.begin(); iter != mIndex.end(); ++iter) { if (listDeleted || !mRecords[iter->second]->isDeleted()) ids.push_back(getRecordId(mRecords[iter->second]->get())); } return ids; } template const Record& Collection::getRecord(const ESM::RefId& id) const { int index = getIndex(id); return *mRecords.at(index); } template const Record& Collection::getRecord(int index) const { return *mRecords.at(index); } 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())); ESM::RefId id = getRecordId(record2->get()); if (index == size) mRecords.push_back(std::move(record2)); else mRecords.insert(mRecords.begin() + index, std::move(record2)); if (index < size - 1) { for (auto& [key, value] : mIndex) { if (value >= index) ++value; } } mIndex.insert(std::make_pair(id, index)); } template void Collection::setRecord(int index, std::unique_ptr> 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) = std::move(record); } template bool Collection::reorderRows(int baseIndex, const std::vector& newOrder) { return false; } } #endif openmw-openmw-0.49.0/apps/opencs/model/world/collectionbase.cpp000066400000000000000000000014201503074453300245700ustar00rootroot00000000000000#include "collectionbase.hpp" #include #include #include #include "columnbase.hpp" 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 columns = getColumns(); for (int i = 0; i < columns; ++i) if (getColumn(i).mColumnId == id) return i; return -1; } int CSMWorld::CollectionBase::findColumnIndex(Columns::ColumnId id) const { int index = searchColumnIndex(id); if (index == -1) throw std::logic_error("invalid column index"); return index; } openmw-openmw-0.49.0/apps/opencs/model/world/collectionbase.hpp000066400000000000000000000105341503074453300246030ustar00rootroot00000000000000#ifndef CSM_WOLRD_COLLECTIONBASE_H #define CSM_WOLRD_COLLECTIONBASE_H #include #include #include #include #include "columns.hpp" #include "universalid.hpp" class QVariant; namespace ESM { class RefId; } namespace CSMWorld { struct ColumnBase; struct RecordBase; /// \brief Base class for record collections /// /// \attention Modifying records through the interface does not update connected views. /// Such modifications should be done through the table model interface instead unless no views /// are connected to the model or special precautions have been taken to send update signals /// manually. class CollectionBase { public: CollectionBase() = default; CollectionBase(const CollectionBase&) = delete; CollectionBase& operator=(const CollectionBase&) = delete; virtual ~CollectionBase() = default; virtual int getSize() const = 0; virtual ESM::RefId getId(int index) const = 0; virtual int getIndex(const ESM::RefId& id) const = 0; virtual int getColumns() const = 0; virtual const ColumnBase& getColumn(int column) const = 0; virtual QVariant getData(int index, int column) const = 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. // virtual void purge() = 0; ///< Remove records that are flagged as erased. 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 int searchId(const ESM::RefId& 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 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 cloneRecord(const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) = 0; virtual bool touchRecord(const ESM::RefId& id) = 0; virtual const RecordBase& getRecord(const ESM::RefId& id) 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 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 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 findColumnIndex(Columns::ColumnId id) const; ///< Return index of column with the given \a id. If no such column exists, an exception is /// thrown. }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/columnbase.cpp000066400000000000000000000072071503074453300237430ustar00rootroot00000000000000#include "columnbase.hpp" #include #include "columns.hpp" CSMWorld::ColumnBase::ColumnBase(int columnId, Display displayType, int flags) : mColumnId(columnId) , mFlags(flags) , mDisplayType(displayType) { } bool CSMWorld::ColumnBase::isUserEditable() const { return isEditable(); } std::string CSMWorld::ColumnBase::getTitle() const { return Columns::getName(static_cast(mColumnId)); } int CSMWorld::ColumnBase::getId() const { return mColumnId; } bool CSMWorld::ColumnBase::isId(Display display) { static const Display ids[] = { Display_Skill, Display_Class, Display_Faction, Display_Rank, Display_Race, Display_Sound, Display_Region, Display_Birthsign, Display_Spell, Display_Cell, Display_Referenceable, Display_Activator, Display_Potion, Display_Apparatus, Display_Armor, Display_Book, Display_Clothing, Display_Container, Display_Creature, Display_Door, Display_Ingredient, Display_CreatureLevelledList, Display_ItemLevelledList, Display_Light, Display_Lockpick, Display_Miscellaneous, Display_Npc, Display_Probe, Display_Repair, Display_Static, Display_Weapon, Display_Reference, Display_Filter, Display_Topic, Display_Journal, Display_TopicInfo, Display_JournalInfo, Display_Scene, Display_GlobalVariable, Display_BodyPart, Display_Enchantment, Display_Script, Display_Mesh, Display_Icon, Display_Music, Display_SoundRes, Display_Texture, Display_Video, Display_Id, Display_SkillId, Display_EffectRange, Display_EffectId, Display_PartRefType, Display_AiPackageType, Display_InfoCondFunc, Display_InfoCondVar, Display_InfoCondComp, Display_EffectSkill, Display_EffectAttribute, Display_IngredEffectId, Display_None, }; for (int i = 0; ids[i] != Display_None; ++i) if (ids[i] == display) return true; return false; } bool CSMWorld::ColumnBase::isText(Display display) { return display == Display_String || display == Display_LongString || display == Display_String32 || display == Display_String64 || display == Display_LongString256; } bool CSMWorld::ColumnBase::isScript(Display display) { return display == Display_ScriptFile || display == Display_ScriptLines; } void CSMWorld::NestableColumn::addColumn(CSMWorld::NestableColumn* column) { mNestedColumns.push_back(column); } const CSMWorld::ColumnBase& CSMWorld::NestableColumn::nestedColumn(int subColumn) const { if (mNestedColumns.empty()) throw std::logic_error("Tried to access nested column of the non-nest column"); return *mNestedColumns.at(subColumn); } CSMWorld::NestableColumn::NestableColumn(int columnId, CSMWorld::ColumnBase::Display displayType, int flag) : CSMWorld::ColumnBase(columnId, displayType, flag) { } CSMWorld::NestableColumn::~NestableColumn() { for (auto* col : mNestedColumns) { delete col; } } 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) { } bool CSMWorld::NestedChildColumn::isEditable() const { return mIsEditable; } openmw-openmw-0.49.0/apps/opencs/model/world/columnbase.hpp000066400000000000000000000163501503074453300237470ustar00rootroot00000000000000#ifndef CSM_WOLRD_COLUMNBASE_H #define CSM_WOLRD_COLUMNBASE_H #include #include #include #include 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 }; enum Roles { Role_Flags = Qt::UserRole, Role_Display = Qt::UserRole + 1, Role_ColumnId = Qt::UserRole + 2 }; enum Flags { Flag_Table = 1, // column should be displayed in table view Flag_Dialogue = 2, // column should be displayed in dialogue view Flag_Dialogue_List = 4, // column should be diaplyed in dialogue view Flag_Dialogue_Refresh = 8 // refresh dialogue view if this column is modified }; enum Display { Display_None, // Do not use Display_String, Display_LongString, // CONCRETE TYPES STARTS HERE (for drag and drop) Display_Skill, Display_Class, Display_Faction, Display_Rank, Display_Race, Display_Sound, Display_Region, Display_Birthsign, Display_Spell, Display_Cell, Display_Referenceable, Display_Activator, Display_Potion, Display_Apparatus, Display_Armor, Display_Book, Display_Clothing, Display_Container, Display_Creature, Display_Door, Display_Ingredient, Display_CreatureLevelledList, Display_ItemLevelledList, Display_Light, Display_Lockpick, Display_Miscellaneous, Display_Npc, Display_Probe, Display_Repair, Display_Static, Display_Weapon, Display_Reference, Display_Filter, Display_Topic, Display_Journal, Display_TopicInfo, Display_JournalInfo, Display_Scene, Display_GlobalVariable, Display_BodyPart, Display_Enchantment, // CONCRETE TYPES ENDS HERE Display_SignedInteger8, Display_SignedInteger16, Display_UnsignedInteger8, Display_UnsignedInteger16, Display_Integer, Display_Float, Display_Double, Display_Var, Display_GmstVarType, Display_GlobalVarType, Display_Specialisation, Display_Attribute, Display_Boolean, Display_SpellType, Display_Script, Display_ApparatusType, Display_ArmorType, Display_ClothingType, Display_CreatureType, Display_WeaponType, Display_RecordState, Display_RefRecordType, Display_DialogueType, Display_QuestStatusType, Display_EnchantmentType, Display_BodyPartType, Display_MeshType, Display_Gender, Display_Mesh, Display_Icon, Display_Music, Display_SoundRes, Display_Texture, Display_Video, Display_Colour, Display_ScriptFile, Display_ScriptLines, // console context Display_SoundGeneratorType, Display_School, Display_Id, Display_SkillId, Display_EffectRange, Display_EffectId, Display_PartRefType, Display_AiPackageType, Display_InfoCondFunc, 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_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 // top level columns that nest other columns Display_NestedHeader }; int mColumnId; int mFlags; Display mDisplayType; ColumnBase(int columnId, Display displayType, int flag); virtual ~ColumnBase() = default; virtual bool isEditable() const = 0; virtual bool isUserEditable() const; ///< Can this column be edited directly by the user? virtual std::string getTitle() const; virtual int getId() const; static bool isId(Display display); static bool isText(Display display); static bool isScript(Display display); }; class NestableColumn : public ColumnBase { std::vector mNestedColumns; public: NestableColumn(int columnId, Display displayType, int flag); ~NestableColumn(); void addColumn(CSMWorld::NestableColumn* column); const ColumnBase& nestedColumn(int subColumn) const; bool hasChildren() const; }; template struct Column : public NestableColumn { Column(int columnId, Display displayType, int flags = Flag_Table | Flag_Dialogue) : NestableColumn(columnId, displayType, flags) { } virtual QVariant get(const Record& record) const = 0; virtual void set(Record& record, const QVariant& data) { throw std::logic_error("Column " + getTitle() + " is not editable"); } }; template struct NestedParentColumn : public Column { 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 { // There is nothing to do here. // This prevents exceptions from parent's implementation } QVariant get(const Record& record) const override { // by default editable; also see IdTree::hasChildren() if (mFixedRows) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); else return QVariant::fromValue(ColumnBase::TableEdit_Full); } bool isEditable() const override { return true; } private: bool mFixedRows; }; struct NestedChildColumn : public NestableColumn { NestedChildColumn(int id, Display display, int flags = ColumnBase::Flag_Dialogue, bool isEditable = true); bool isEditable() const override; private: bool mIsEditable; }; } Q_DECLARE_METATYPE(CSMWorld::ColumnBase::TableEditModes) #endif openmw-openmw-0.49.0/apps/opencs/model/world/columnimp.cpp000066400000000000000000000223021503074453300236070ustar00rootroot00000000000000#include "columnimp.hpp" #include #include #include #include #include #include #include #include #include #include namespace CSMWorld { namespace { struct GetStringId { std::string operator()(ESM::EmptyRefId /*value*/) const { return std::string(); } std::string operator()(ESM::StringRefId value) const { return value.getValue(); } std::string operator()(ESM::FormId value) const { return value.toString("FormId:"); } 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) { } QVariant LandTextureIndexColumn::get(const Record& record) const { return record.get().mIndex; } bool LandTextureIndexColumn::isEditable() const { return false; } /* LandPluginIndexColumn */ LandPluginIndexColumn::LandPluginIndexColumn() : Column(Columns::ColumnId_PluginIndex, ColumnBase::Display_Integer, 0) { } QVariant LandPluginIndexColumn::get(const Record& record) const { return record.get().getPlugin(); } bool LandPluginIndexColumn::isEditable() const { return false; } /* LandNormalsColumn */ LandNormalsColumn::LandNormalsColumn() : Column(Columns::ColumnId_LandNormalsIndex, ColumnBase::Display_String, 0) { } QVariant LandNormalsColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS * 3; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VNML)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mNormals[i]; } QVariant variant; variant.setValue(values); return variant; } void LandNormalsColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS * 3) throw std::runtime_error("invalid land normals data"); Land copy = record.get(); copy.add(Land::DATA_VNML); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mNormals[i] = values[i]; } record.setModified(copy); } bool LandNormalsColumn::isEditable() const { return true; } /* LandHeightsColumn */ LandHeightsColumn::LandHeightsColumn() : Column(Columns::ColumnId_LandHeightsIndex, ColumnBase::Display_String, 0) { } QVariant LandHeightsColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VHGT)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mHeights[i]; } QVariant variant; variant.setValue(values); return variant; } void LandHeightsColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS) throw std::runtime_error("invalid land heights data"); Land copy = record.get(); copy.add(Land::DATA_VHGT); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mHeights[i] = values[i]; } copy.mFlags |= Land::Flag_HeightsNormals; record.setModified(copy); } bool LandHeightsColumn::isEditable() const { return true; } /* LandColoursColumn */ LandColoursColumn::LandColoursColumn() : Column(Columns::ColumnId_LandColoursIndex, ColumnBase::Display_String, 0) { } QVariant LandColoursColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_VERTS * 3; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VCLR)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mColours[i]; } QVariant variant; variant.setValue(values); return variant; } void LandColoursColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_VERTS * 3) throw std::runtime_error("invalid land colours data"); Land copy = record.get(); copy.add(Land::DATA_VCLR); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mColours[i] = values[i]; } copy.mFlags |= Land::Flag_Colors; record.setModified(copy); } bool LandColoursColumn::isEditable() const { return true; } /* LandTexturesColumn */ LandTexturesColumn::LandTexturesColumn() : Column(Columns::ColumnId_LandTexturesIndex, ColumnBase::Display_String, 0) { } QVariant LandTexturesColumn::get(const Record& record) const { const int Size = Land::LAND_NUM_TEXTURES; const Land& land = record.get(); DataType values(Size, 0); if (land.isDataLoaded(Land::DATA_VTEX)) { for (int i = 0; i < Size; ++i) values[i] = land.getLandData()->mTextures[i]; } QVariant variant; variant.setValue(values); return variant; } void LandTexturesColumn::set(Record& record, const QVariant& data) { DataType values = data.value(); if (values.size() != Land::LAND_NUM_TEXTURES) throw std::runtime_error("invalid land textures data"); Land copy = record.get(); copy.add(Land::DATA_VTEX); for (int i = 0; i < values.size(); ++i) { copy.getLandData()->mTextures[i] = values[i]; } copy.mFlags |= Land::Flag_Textures; record.setModified(copy); } bool LandTexturesColumn::isEditable() const { return true; } /* BodyPartRaceColumn */ BodyPartRaceColumn::BodyPartRaceColumn(const MeshTypeColumn* meshType) : mMeshType(meshType) { } QVariant BodyPartRaceColumn::get(const Record& record) const { if (mMeshType != nullptr && mMeshType->get(record) == ESM::BodyPart::MT_Skin) { return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); } return DisableTag::getVariant(); } void BodyPartRaceColumn::set(Record& record, const QVariant& data) { ESM::BodyPart record2 = record.get(); record2.mRace = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool BodyPartRaceColumn::isEditable() const { 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); } } openmw-openmw-0.49.0/apps/opencs/model/world/columnimp.hpp000066400000000000000000002220421503074453300236170ustar00rootroot00000000000000#ifndef CSM_WOLRD_COLUMNIMP_H #define CSM_WOLRD_COLUMNIMP_H #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 "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 struct FloatValueColumn : public Column { FloatValueColumn() : Column(Columns::ColumnId_Value, ColumnBase::Display_Float) { } 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); } bool isEditable() const override { return true; } }; 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 { return QString::fromStdString(getStringId(record.get().mId)); } bool isEditable() const override { return false; } }; 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 struct RecordStateColumn : public Column { RecordStateColumn() : Column(Columns::ColumnId_Modification, ColumnBase::Display_RecordState) { } QVariant get(const Record& record) const override { 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 { record.mState = static_cast(data.toInt()); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; 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 { return mType; } bool isEditable() const override { return false; } }; /// \attention A var type column must be immediately followed by a suitable value column. 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 { return static_cast(record.get().mValue.getType()); } void set(Record& record, const QVariant& data) override { 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 struct VarValueColumn : public Column { 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 { switch (record.get().mValue.getType()) { case ESM::VT_String: return QString::fromUtf8(record.get().mValue.getString().c_str()); case ESM::VT_Int: case ESM::VT_Short: case ESM::VT_Long: return record.get().mValue.getInteger(); case ESM::VT_Float: return record.get().mValue.getFloat(); default: return QVariant(); } } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); switch (record2.mValue.getType()) { case ESM::VT_String: 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()); break; case ESM::VT_Float: record2.mValue.setFloat(data.toFloat()); break; default: break; } record.setModified(record2); } bool isEditable() const override { return true; } }; template struct DescriptionColumn : public Column { DescriptionColumn() : Column(Columns::ColumnId_Description, ColumnBase::Display_LongString) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mDescription.c_str()); } 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; } }; template struct SpecialisationColumn : public Column { SpecialisationColumn() : Column(Columns::ColumnId_Specialisation, ColumnBase::Display_Specialisation) { } 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); } bool isEditable() const override { return true; } }; 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 { 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); } bool isEditable() const override { return true; } }; template struct AttributeColumn : public Column { AttributeColumn() : Column(Columns::ColumnId_Attribute, ColumnBase::Display_Attribute) { } 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); } 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()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mName = data.toString().toUtf8().constData(); 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()); } void set(Record& record, const QVariant& data) override { CSMWorld::Cell record2 = record.get(); record2.mName = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; 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 { 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); } bool isEditable() const override { return true; } }; 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 { return QString::fromStdString( ESM::Skill::indexToRefId(record.get().mData.getSkill(mIndex, mMajor)).getRefIdString()); } 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) = static_cast(*index); record.setModified(record2); } } bool isEditable() const override { return true; } }; template struct PlayableColumn : public Column { PlayableColumn() : Column(Columns::ColumnId_Playable, ColumnBase::Display_Boolean) { } 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); } bool isEditable() const override { return true; } }; template struct HiddenColumn : public Column { HiddenColumn() : Column(Columns::ColumnId_Hidden, ColumnBase::Display_Boolean) { } 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); } bool isEditable() const override { return true; } }; 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) { } QVariant get(const Record& record) const override { bool flag = (record.get().mData.mFlags & mMask) != 0; if (mInverted) flag = !flag; return flag; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mData.mFlags & ~mMask; if ((data.toInt() != 0) != mInverted) flags |= mMask; record2.mData.mFlags = flags; record.setModified(record2); } bool isEditable() const override { return true; } }; 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) { } QVariant get(const Record& record) const override { bool flag = (record.get().mFlags & mMask) != 0; if (mInverted) flag = !flag; return flag; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); int flags = record2.mFlags & ~mMask; if ((data.toInt() != 0) != mInverted) flags |= mMask; record2.mFlags = flags; record.setModified(record2); } bool isEditable() const override { return true; } }; 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 { 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 { ESXRecordT record2 = record.get(); float bodyAttr = std::clamp(data.toFloat(), 0.5f, 2.0f); 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; } }; template struct SoundParamColumn : public Column { enum Type { Type_Volume, Type_MinRange, Type_MaxRange }; 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) { } 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; } return value; } void set(Record& record, const QVariant& data) override { int value = data.toInt(); if (value < 0) value = 0; 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; } record.setModified(record2); } bool isEditable() const override { return true; } }; template struct SoundFileColumn : public Column { SoundFileColumn() : Column(Columns::ColumnId_SoundFile, ColumnBase::Display_SoundRes) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mSound.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSound = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct MapColourColumn : public Column { MapColourColumn() : Column(Columns::ColumnId_MapColour, ColumnBase::Display_Colour) { } 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); } bool isEditable() const override { return true; } }; template struct SleepListColumn : public Column { SleepListColumn() : Column(Columns::ColumnId_SleepEncounter, ColumnBase::Display_CreatureLevelledList) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mSleepList.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSleepList = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct TextureColumn : public Column { TextureColumn() : Column(Columns::ColumnId_Texture, ColumnBase::Display_Texture) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mTexture.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTexture = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct SpellTypeColumn : public Column { SpellTypeColumn() : Column(Columns::ColumnId_SpellType, ColumnBase::Display_SpellType) { } 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); } bool isEditable() const override { return true; } }; template struct CostColumn : public Column { CostColumn() : Column(Columns::ColumnId_Cost, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct ScriptColumn : public Column { enum Type { Type_File, // regular script record Type_Lines, // console context 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) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mScriptText.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mScriptText = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct RegionColumn : public Column { RegionColumn() : Column(Columns::ColumnId_Region, ColumnBase::Display_Region) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mRegion.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRegion = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; 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) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mCell.toString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mCell = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return !mBlocked; } }; template struct OriginalCellColumn : public Column { OriginalCellColumn() : Column(Columns::ColumnId_OriginalCell, ColumnBase::Display_Cell) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mOriginalCell.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mOriginalCell = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct IdColumn : public Column { IdColumn() : Column(Columns::ColumnId_ReferenceableId, ColumnBase::Display_Referenceable) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mRefID.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefID = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct ScaleColumn : public Column { ScaleColumn() : Column(Columns::ColumnId_Scale, ColumnBase::Display_Float) { } 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 = std::clamp(data.toFloat(), 0.5f, 2.0f); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct OwnerColumn : public Column { OwnerColumn() : Column(Columns::ColumnId_Owner, ColumnBase::Display_Npc) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mOwner.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mOwner = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct SoulColumn : public Column { SoulColumn() : Column(Columns::ColumnId_Soul, ColumnBase::Display_Creature) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mSoul.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSoul = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct FactionColumn : public Column { FactionColumn() : Column(Columns::ColumnId_Faction, ColumnBase::Display_Faction) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mFaction.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFaction = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct FactionIndexColumn : public Column { FactionIndexColumn() : Column(Columns::ColumnId_FactionIndex, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct ChargesColumn : public Column { ChargesColumn() : Column(Columns::ColumnId_Charges, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct EnchantmentChargesColumn : public Column { EnchantmentChargesColumn() : Column(Columns::ColumnId_Enchantment, ColumnBase::Display_Float) { } 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); } bool isEditable() const override { return true; } }; template struct StackSizeColumn : public Column { StackSizeColumn() : Column(Columns::ColumnId_StackCount, ColumnBase::Display_Integer) { } QVariant get(const Record& record) const override { return record.get().mCount; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mCount = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct TeleportColumn : public Column { TeleportColumn(int flags) : Column(Columns::ColumnId_Teleport, ColumnBase::Display_Boolean, flags) { } 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); } bool isEditable() const override { return true; } }; template struct TeleportCellColumn : public Column { TeleportCellColumn() : Column(Columns::ColumnId_TeleportCell, ColumnBase::Display_Cell) { } QVariant get(const Record& record) const override { if (!record.get().mTeleport) return QVariant(); return QString::fromUtf8(record.get().mDestCell.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mDestCell = data.toString().toUtf8().constData(); record.setModified(record2); } 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) { } QVariant get(const Record& record) const override { return record.get().mIsLocked; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mIsLocked = data.toBool(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct LockLevelColumn : public Column { LockLevelColumn() : Column(Columns::ColumnId_LockLevel, ColumnBase::Display_Integer) { } QVariant get(const Record& record) const override { if (record.get().mIsLocked) return record.get().mLockLevel; return QVariant(); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mLockLevel = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct KeyColumn : public Column { KeyColumn() : Column(Columns::ColumnId_Key, ColumnBase::Display_Miscellaneous) { } QVariant get(const Record& record) const override { if (record.get().mIsLocked) return QString::fromUtf8(record.get().mKey.getRefIdString().c_str()); return QVariant(); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mKey = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct TrapColumn : public Column { TrapColumn() : Column(Columns::ColumnId_Trap, ColumnBase::Display_Spell) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mTrap.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTrap = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct FilterColumn : public Column { FilterColumn() : Column(Columns::ColumnId_Filter, ColumnBase::Display_Filter) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mFilter.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mFilter = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct PosColumn : public Column { 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) , mIsDoor(door) { } 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 { ESXRecordT record2 = record.get(); ESM::Position& position = record2.*mPosition; position.pos[mIndex] = data.toFloat(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct RotColumn : public Column { 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) , mIsDoor(door) { } 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 { ESXRecordT record2 = record.get(); ESM::Position& position = record2.*mPosition; position.rot[mIndex] = osg::DegreesToRadians(data.toFloat()); record.setModified(record2); } bool isEditable() const override { return true; } }; 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 { return static_cast(record.get().mType); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mType = static_cast(data.toInt()); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct QuestStatusTypeColumn : public Column { QuestStatusTypeColumn() : Column(Columns::ColumnId_QuestStatusType, ColumnBase::Display_QuestStatusType) { } QVariant get(const Record& record) const override { return static_cast(record.get().mQuestStatus); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mQuestStatus = static_cast(data.toInt()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct QuestDescriptionColumn : public Column { QuestDescriptionColumn() : Column(Columns::ColumnId_QuestDescription, ColumnBase::Display_LongString) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mResponse.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct QuestIndexColumn : public Column { QuestIndexColumn() : Column(Columns::ColumnId_QuestIndex, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct TopicColumn : public Column { 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 { return QString::fromUtf8(record.get().mTopicId.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mTopicId = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct ActorColumn : public Column { ActorColumn() : Column(Columns::ColumnId_Actor, ColumnBase::Display_Npc) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mActor.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mActor = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct RaceColumn : public Column { RaceColumn() : Column(Columns::ColumnId_Race, ColumnBase::Display_Race) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mRace.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRace = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct ClassColumn : public Column { ClassColumn() : Column(Columns::ColumnId_Class, ColumnBase::Display_Class) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mClass.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mClass = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct PcFactionColumn : public Column { PcFactionColumn() : Column(Columns::ColumnId_PcFaction, ColumnBase::Display_Faction) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mPcFaction.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mPcFaction = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct ResponseColumn : public Column { ResponseColumn() : Column(Columns::ColumnId_Response, ColumnBase::Display_LongString) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mResponse.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mResponse = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct DispositionColumn : public Column { DispositionColumn() : Column(Columns::ColumnId_Disposition, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct RankColumn : public Column { RankColumn() : Column(Columns::ColumnId_Rank, ColumnBase::Display_Integer) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mRank); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mRank = static_cast(data.toInt()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct PcRankColumn : public Column { PcRankColumn() : Column(Columns::ColumnId_PcRank, ColumnBase::Display_Integer) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mPCrank); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mPCrank = static_cast(data.toInt()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct GenderColumn : public Column { GenderColumn() : Column(Columns::ColumnId_Gender, ColumnBase::Display_Gender) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mGender); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mGender = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct GenderNpcColumn : public Column { GenderNpcColumn() : Column(Columns::ColumnId_Gender, ColumnBase::Display_GenderNpc) { } QVariant get(const Record& record) const override { // Implemented this way to allow additional gender types in the future. if ((record.get().mData.mFlags & ESM::BodyPart::BPF_Female) == ESM::BodyPart::BPF_Female) return 1; return 0; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); // Implemented this way to allow additional gender types in the future. if (data.toInt() == 1) record2.mData.mFlags = (record2.mData.mFlags & ~ESM::BodyPart::BPF_Female) | ESM::BodyPart::BPF_Female; else record2.mData.mFlags = record2.mData.mFlags & ~ESM::BodyPart::BPF_Female; record.setModified(record2); } bool isEditable() const override { return true; } }; template struct EnchantmentTypeColumn : public Column { EnchantmentTypeColumn() : Column(Columns::ColumnId_EnchantmentType, ColumnBase::Display_EnchantmentType) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mType); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct ChargesColumn2 : public Column { ChargesColumn2() : Column(Columns::ColumnId_Charges, ColumnBase::Display_Integer) { } 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); } bool isEditable() const override { return true; } }; template struct AutoCalcColumn : public Column { AutoCalcColumn() : Column(Columns::ColumnId_AutoCalc, ColumnBase::Display_Boolean) { } 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); } bool isEditable() const override { return true; } }; template struct ModelColumn : public Column { ModelColumn() : Column(Columns::ColumnId_Model, ColumnBase::Display_Mesh) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mModel.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mModel = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct VampireColumn : public Column { VampireColumn() : Column(Columns::ColumnId_Vampire, ColumnBase::Display_Boolean) { } 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); } bool isEditable() const override { return true; } }; template struct BodyPartTypeColumn : public Column { BodyPartTypeColumn() : Column(Columns::ColumnId_BodyPartType, ColumnBase::Display_BodyPartType) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mPart); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mPart = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct MeshTypeColumn : public Column { MeshTypeColumn(int flags = ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue) : Column(Columns::ColumnId_MeshType, ColumnBase::Display_MeshType, flags) { } QVariant get(const Record& record) const override { return static_cast(record.get().mData.mType); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mType = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct OwnerGlobalColumn : public Column { OwnerGlobalColumn() : Column(Columns::ColumnId_OwnerGlobal, ColumnBase::Display_GlobalVariable) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mGlobalVariable.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mGlobalVariable = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct RefNumCounterColumn : public Column { RefNumCounterColumn() : Column(Columns::ColumnId_RefNumCounter, ColumnBase::Display_Integer, 0) { } QVariant get(const Record& record) const override { return static_cast(record.get().mRefNumCounter); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNumCounter = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct RefNumColumn : public Column { RefNumColumn() : Column(Columns::ColumnId_RefNum, ColumnBase::Display_Integer, 0) { } QVariant get(const Record& record) const override { return static_cast(record.get().mRefNum.mIndex); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mRefNum.mIndex = data.toInt(); record.setModified(record2); } bool isEditable() const override { return true; } bool isUserEditable() const override { return false; } }; template struct SoundColumn : public Column { SoundColumn() : Column(Columns::ColumnId_Sound, ColumnBase::Display_Sound) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mSound.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mSound = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct CreatureColumn : public Column { CreatureColumn() : Column(Columns::ColumnId_Creature, ColumnBase::Display_Creature) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mCreature.getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mCreature = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct SoundGeneratorTypeColumn : public Column { SoundGeneratorTypeColumn() : Column(Columns::ColumnId_SoundGeneratorType, ColumnBase::Display_SoundGeneratorType) { } 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); } bool isEditable() const override { return true; } }; template struct BaseCostColumn : public Column { BaseCostColumn() : Column(Columns::ColumnId_BaseCost, ColumnBase::Display_Float) { } 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); } bool isEditable() const override { return true; } }; template struct ProjectileSpeedColumn : public Column { ProjectileSpeedColumn() : Column(Columns::ColumnId_ProjectileSpeed, ColumnBase::Display_Float) { } QVariant get(const Record& record) const override { return record.get().mData.mSpeed; } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mSpeed = data.toFloat(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct SchoolColumn : public Column { SchoolColumn() : Column(Columns::ColumnId_School, ColumnBase::Display_School) { } QVariant get(const Record& record) const override { return ESM::MagicSchool::skillRefIdToIndex(record.get().mData.mSchool); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mData.mSchool = ESM::MagicSchool::indexToSkillRefId(data.toInt()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct EffectTextureColumn : public Column { 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); } QVariant get(const Record& record) const override { return QString::fromUtf8( (this->mColumnId == Columns::ColumnId_Icon ? record.get().mIcon : record.get().mParticle).c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); (this->mColumnId == Columns::ColumnId_Icon ? record2.mIcon : record2.mParticle) = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct EffectObjectColumn : public Column { 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); } QVariant get(const Record& record) const override { 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; } if (!string) throw std::logic_error("Unsupported column ID"); return QString::fromUtf8(string->getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESM::RefId* id = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { 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 (!id) throw std::logic_error("Unsupported column ID"); *id = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct EffectSoundColumn : public Column { 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); } QVariant get(const Record& record) const override { const ESM::RefId* id = nullptr; switch (this->mColumnId) { 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 (!id) throw std::logic_error("Unsupported column ID"); return QString::fromUtf8(id->getRefIdString().c_str()); } void set(Record& record, const QVariant& data) override { ESM::RefId* id = nullptr; ESXRecordT record2 = record.get(); switch (this->mColumnId) { 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 (!id) throw std::logic_error("Unsupported column ID"); *id = ESM::RefId::stringRefId(data.toString().toUtf8().constData()); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct FormatColumn : public Column { FormatColumn() : Column(Columns::ColumnId_FileFormat, ColumnBase::Display_Integer) { } QVariant get(const Record& record) const override { return record.get().mFormatVersion; } bool isEditable() const override { return false; } }; template struct AuthorColumn : public Column { AuthorColumn() : Column(Columns::ColumnId_Author, ColumnBase::Display_String32) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mAuthor.c_str()); } void set(Record& record, const QVariant& data) override { ESXRecordT record2 = record.get(); record2.mAuthor = data.toString().toUtf8().constData(); record.setModified(record2); } bool isEditable() const override { return true; } }; template struct FileDescriptionColumn : public Column { FileDescriptionColumn() : Column(Columns::ColumnId_FileDescription, ColumnBase::Display_LongString256) { } QVariant get(const Record& record) const override { return QString::fromUtf8(record.get().mDescription.c_str()); } 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; } }; struct LandTextureIndexColumn : public Column { LandTextureIndexColumn(); QVariant get(const Record& record) const override; bool isEditable() const override; }; struct LandPluginIndexColumn : public Column { LandPluginIndexColumn(); QVariant get(const Record& record) const override; bool isEditable() const override; }; struct LandNormalsColumn : public Column { using DataType = QVector; LandNormalsColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandHeightsColumn : public Column { using DataType = QVector; LandHeightsColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandColoursColumn : public Column { using DataType = QVector; LandColoursColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct LandTexturesColumn : public Column { using DataType = QVector; LandTexturesColumn(); QVariant get(const Record& record) const override; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; struct BodyPartRaceColumn : public RaceColumn { 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; void set(Record& record, const QVariant& data) override; bool isEditable() const override; }; } // This is required to access the type as a QVariant. Q_DECLARE_METATYPE(CSMWorld::LandNormalsColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandHeightsColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandColoursColumn::DataType) Q_DECLARE_METATYPE(CSMWorld::LandTexturesColumn::DataType) #endif openmw-openmw-0.49.0/apps/opencs/model/world/columns.cpp000066400000000000000000000665651503074453300233070ustar00rootroot00000000000000#include "columns.hpp" #include #include #include #include #include "infoselectwrapper.hpp" #include "universalid.hpp" namespace CSMWorld { namespace Columns { struct ColumnDesc { int mId; const char* mName; }; const ColumnDesc sNames[] = { { ColumnId_Value, "Value" }, { ColumnId_Id, "ID" }, { ColumnId_Modification, "Modified" }, { ColumnId_RecordType, "Record Type" }, { ColumnId_ValueType, "Value Type" }, { ColumnId_Description, "Description" }, { ColumnId_Specialisation, "Specialisation" }, { ColumnId_Attribute, "Attribute" }, { ColumnId_Name, "Name" }, { ColumnId_Playable, "Playable" }, { ColumnId_Hidden, "Hidden" }, { ColumnId_MaleWeight, "Male Weight" }, { ColumnId_FemaleWeight, "Female Weight" }, { ColumnId_MaleHeight, "Male Height" }, { ColumnId_FemaleHeight, "Female Height" }, { ColumnId_Volume, "Volume" }, { ColumnId_MinRange, "Min Range" }, { ColumnId_MaxRange, "Max Range" }, { ColumnId_MinMagnitude, "Min Magnitude" }, { ColumnId_MaxMagnitude, "Max Magnitude" }, { ColumnId_SoundFile, "Sound File" }, { ColumnId_MapColour, "Map Colour" }, { ColumnId_SleepEncounter, "Sleep Encounter" }, { ColumnId_Texture, "Texture" }, { ColumnId_SpellType, "Spell Type" }, { ColumnId_Cost, "Cost" }, { ColumnId_ScriptText, "Script Text" }, { ColumnId_Region, "Region" }, { ColumnId_Cell, "Cell" }, { ColumnId_Scale, "Scale" }, { ColumnId_Owner, "Owner" }, { ColumnId_Soul, "Soul" }, { ColumnId_Faction, "Faction" }, { ColumnId_FactionIndex, "Faction Index" }, { ColumnId_Charges, "Charges" }, { ColumnId_Enchantment, "Enchantment" }, { 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" }, { ColumnId_BeastRace, "Beast Race" }, { ColumnId_AutoCalc, "Auto Calc" }, { ColumnId_StarterSpell, "Starter Spell" }, { ColumnId_AlwaysSucceeds, "Always Succeeds" }, { ColumnId_SleepForbidden, "Sleep Forbidden" }, { ColumnId_InteriorWater, "Interior Water" }, { ColumnId_InteriorSky, "Interior Sky" }, { ColumnId_Model, "Model/Animation" }, { ColumnId_Script, "Script" }, { ColumnId_Icon, "Icon" }, { ColumnId_Weight, "Weight" }, { ColumnId_EnchantmentPoints, "Enchantment Points" }, { ColumnId_Quality, "Quality" }, { ColumnId_AiHello, "AI Hello" }, { ColumnId_AiFlee, "AI Flee" }, { ColumnId_AiFight, "AI Fight" }, { ColumnId_AiAlarm, "AI Alarm" }, { ColumnId_BuysWeapons, "Buys Weapons" }, { ColumnId_BuysArmor, "Buys Armor" }, { ColumnId_BuysClothing, "Buys Clothing" }, { ColumnId_BuysBooks, "Buys Books" }, { ColumnId_BuysIngredients, "Buys Ingredients" }, { ColumnId_BuysLockpicks, "Buys Lockpicks" }, { ColumnId_BuysProbes, "Buys Probes" }, { ColumnId_BuysLights, "Buys Lights" }, { ColumnId_BuysApparati, "Buys Apparati" }, { ColumnId_BuysRepairItems, "Buys Repair Items" }, { ColumnId_BuysMiscItems, "Buys Misc Items" }, { ColumnId_BuysPotions, "Buys Potions" }, { ColumnId_BuysMagicItems, "Buys Magic Items" }, { ColumnId_SellsSpells, "Sells Spells" }, { ColumnId_Trainer, "Trainer" }, { ColumnId_Spellmaking, "Spellmaking" }, { ColumnId_EnchantingService, "Enchanting Service" }, { ColumnId_RepairService, "Repair Service" }, { ColumnId_ApparatusType, "Apparatus Type" }, { ColumnId_ArmorType, "Armor Type" }, { ColumnId_Health, "Health" }, { ColumnId_ArmorValue, "Armor Value" }, { ColumnId_BookType, "Book Type" }, { ColumnId_ClothingType, "Clothing Type" }, { ColumnId_WeightCapacity, "Weight Capacity" }, { ColumnId_OrganicContainer, "Organic Container" }, { ColumnId_Respawn, "Respawn" }, { ColumnId_CreatureType, "Creature Type" }, { ColumnId_SoulPoints, "Soul Points" }, { ColumnId_ParentCreature, "Parent Creature" }, { ColumnId_Biped, "Biped" }, { ColumnId_HasWeapon, "Has Weapon" }, { ColumnId_Swims, "Swims" }, { ColumnId_Flies, "Flies" }, { ColumnId_Walks, "Walks" }, { ColumnId_Essential, "Essential" }, { ColumnId_BloodType, "Blood Type" }, { ColumnId_OpenSound, "Open Sound" }, { ColumnId_CloseSound, "Close Sound" }, { ColumnId_Duration, "Duration" }, { ColumnId_Radius, "Radius" }, { ColumnId_Colour, "Colour" }, { ColumnId_Sound, "Sound" }, { ColumnId_Dynamic, "Dynamic" }, { ColumnId_Portable, "Portable" }, { ColumnId_NegativeLight, "Negative Light" }, { ColumnId_EmitterType, "Emitter Type" }, { ColumnId_Fire, "Fire" }, { ColumnId_OffByDefault, "Off by default" }, { ColumnId_IsKey, "Is Key" }, { ColumnId_Race, "Race" }, { ColumnId_Class, "Class" }, { Columnid_Hair, "Hair" }, { ColumnId_Head, "Head" }, { ColumnId_Female, "Female" }, { ColumnId_WeaponType, "Weapon Type" }, { ColumnId_WeaponSpeed, "Weapon Speed" }, { ColumnId_WeaponReach, "Weapon Reach" }, { ColumnId_MinChop, "Min Chop" }, { ColumnId_MaxChip, "Max Chop" }, { Columnid_MinSlash, "Min Slash" }, { ColumnId_MaxSlash, "Max Slash" }, { ColumnId_MinThrust, "Min Thrust" }, { ColumnId_MaxThrust, "Max Thrust" }, { ColumnId_Magical, "Magical" }, { ColumnId_Silver, "Silver" }, { ColumnId_Filter, "Filter" }, { ColumnId_PositionXPos, "Pos X" }, { ColumnId_PositionYPos, "Pos Y" }, { ColumnId_PositionZPos, "Pos Z" }, { ColumnId_PositionXRot, "Rot X" }, { ColumnId_PositionYRot, "Rot Y" }, { ColumnId_PositionZRot, "Rot Z" }, { ColumnId_DoorPositionXPos, "Teleport Pos X" }, { ColumnId_DoorPositionYPos, "Teleport Pos Y" }, { ColumnId_DoorPositionZPos, "Teleport Pos Z" }, { ColumnId_DoorPositionXRot, "Teleport Rot X" }, { ColumnId_DoorPositionYRot, "Teleport Rot Y" }, { ColumnId_DoorPositionZRot, "Teleport Rot Z" }, { ColumnId_DialogueType, "Dialogue Type" }, { ColumnId_QuestIndex, "Quest Index" }, { ColumnId_QuestStatusType, "Quest Status" }, { ColumnId_QuestDescription, "Quest Description" }, { ColumnId_Topic, "Topic" }, { ColumnId_Journal, "Journal" }, { ColumnId_Actor, "Actor" }, { ColumnId_PcFaction, "PC Faction" }, { ColumnId_Response, "Response" }, { ColumnId_Disposition, "Disposition" }, { ColumnId_Rank, "Rank" }, { ColumnId_Gender, "Gender" }, { ColumnId_PcRank, "PC Rank" }, { ColumnId_ReferenceableId, "Object ID" }, { ColumnId_ContainerContent, "Content" }, { ColumnId_ItemCount, "Count" }, { ColumnId_InventoryItemId, "Item ID" }, { ColumnId_CombatState, "Combat" }, { ColumnId_MagicState, "Magic" }, { ColumnId_StealthState, "Stealth" }, { ColumnId_EnchantmentType, "Enchantment Type" }, { ColumnId_Vampire, "Vampire" }, { ColumnId_BodyPartType, "Bodypart Type" }, { ColumnId_MeshType, "Mesh Type" }, { ColumnId_ActorInventory, "Inventory" }, { ColumnId_SpellList, "Spells" }, { 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_OwnerGlobal, "Owner Global" }, { ColumnId_DefaultProfile, "Default Profile" }, { ColumnId_BypassNewGame, "Bypass New Game" }, { ColumnId_GlobalProfile, "Global Profile" }, { ColumnId_RefNumCounter, "RefNum Counter" }, { ColumnId_RefNum, "RefNum" }, { ColumnId_Creature, "Creature" }, { ColumnId_SoundGeneratorType, "Sound Generator Type" }, { ColumnId_AllowSpellmaking, "Allow Spellmaking" }, { ColumnId_AllowEnchanting, "Allow Enchanting" }, { ColumnId_BaseCost, "Base Cost" }, { ColumnId_School, "School" }, { ColumnId_Particle, "Particle" }, { ColumnId_CastingObject, "Casting Object" }, { ColumnId_HitObject, "Hit Object" }, { ColumnId_AreaObject, "Area Object" }, { ColumnId_BoltObject, "Bolt Object" }, { ColumnId_CastingSound, "Casting Sound" }, { ColumnId_HitSound, "Hit Sound" }, { ColumnId_AreaSound, "Area Sound" }, { ColumnId_BoltSound, "Bolt Sound" }, { ColumnId_PathgridPoints, "Points" }, { ColumnId_PathgridIndex, "pIndex" }, { ColumnId_PathgridPosX, "X" }, { ColumnId_PathgridPosY, "Y" }, { ColumnId_PathgridPosZ, "Z" }, { ColumnId_PathgridEdges, "Edges" }, { ColumnId_PathgridEdgeIndex, "eIndex" }, { ColumnId_PathgridEdge0, "Point 0" }, { ColumnId_PathgridEdge1, "Point 1" }, { ColumnId_RegionSounds, "Sounds" }, { ColumnId_SoundName, "Sound Name" }, { ColumnId_SoundChance, "Chance" }, { ColumnId_SoundProbability, "Probability" }, { ColumnId_FactionReactions, "Reactions" }, { ColumnId_FactionRanks, "Ranks" }, { ColumnId_FactionReaction, "Reaction" }, { ColumnId_FactionAttrib1, "Attrib 1" }, { ColumnId_FactionAttrib2, "Attrib 2" }, { ColumnId_FactionPrimSkill, "Prim Skill" }, { ColumnId_FactionFavSkill, "Fav Skill" }, { ColumnId_FactionRep, "Fact Rep" }, { ColumnId_RankName, "Rank Name" }, { ColumnId_EffectList, "Effects" }, { ColumnId_EffectId, "Effect" }, { ColumnId_EffectRange, "Range" }, { ColumnId_EffectArea, "Area" }, { ColumnId_AiPackageList, "Ai Packages" }, { ColumnId_AiPackageType, "Package" }, { ColumnId_AiWanderDist, "Wander Dist" }, { ColumnId_AiDuration, "Ai Duration" }, { ColumnId_AiWanderToD, "Wander ToD" }, { ColumnId_AiWanderRepeat, "Ai Repeat" }, { ColumnId_AiActivateName, "Activate" }, { ColumnId_AiTargetId, "Target ID" }, { ColumnId_AiTargetCell, "Target Cell" }, { ColumnId_PartRefList, "Part Reference" }, { ColumnId_PartRefType, "Type" }, { ColumnId_PartRefMale, "Male Part" }, { ColumnId_PartRefFemale, "Female Part" }, { ColumnId_LevelledList, "Levelled List" }, { ColumnId_LevelledItemId, "Levelled Item" }, { ColumnId_LevelledItemLevel, "PC Level" }, { ColumnId_LevelledItemType, "Calculate all levels <= player" }, { ColumnId_LevelledItemTypeEach, "Select a new item for each instance" }, { ColumnId_LevelledItemChanceNone, "Chance None" }, { ColumnId_PowerList, "Powers" }, { ColumnId_Skill, "Skill" }, { ColumnId_InfoList, "Info List" }, { ColumnId_InfoCondition, "Info Conditions" }, { ColumnId_InfoCondFunc, "Function" }, { ColumnId_InfoCondVar, "Variable/Object" }, { ColumnId_InfoCondComp, "Relation" }, { ColumnId_InfoCondValue, "Values" }, { ColumnId_OriginalCell, "Original Cell" }, { ColumnId_NpcAttributes, "NPC Attributes" }, { ColumnId_NpcSkills, "NPC Skill" }, { ColumnId_UChar, "Value [0..255]" }, { ColumnId_NpcMisc, "NPC Misc" }, { ColumnId_Level, "Level" }, { ColumnId_Mana, "Mana" }, { ColumnId_Fatigue, "Fatigue" }, { ColumnId_NpcDisposition, "NPC Disposition" }, { ColumnId_NpcReputation, "Reputation" }, { ColumnId_NpcRank, "NPC Rank" }, { ColumnId_Gold, "Gold" }, { ColumnId_RaceAttributes, "Race Attributes" }, { ColumnId_Male, "Male" }, { ColumnId_RaceSkillBonus, "Skill Bonus" }, { ColumnId_RaceBonus, "Bonus" }, { ColumnId_Interior, "Interior" }, { ColumnId_Ambient, "Ambient" }, { ColumnId_Sunlight, "Sunlight" }, { ColumnId_Fog, "Fog" }, { ColumnId_FogDensity, "Fog Density" }, { ColumnId_WaterLevel, "Water Level" }, { ColumnId_MapColor, "Map Color" }, { ColumnId_FileFormat, "File Format" }, { ColumnId_FileDescription, "File Description" }, { ColumnId_Author, "Author" }, { ColumnId_CreatureAttributes, "Creature Attributes" }, { ColumnId_AttributeValue, "Attrib Value" }, { ColumnId_CreatureAttack, "Creature Attack" }, { ColumnId_MinAttack, "Min Attack" }, { ColumnId_MaxAttack, "Max Attack" }, { ColumnId_CreatureMisc, "Creature Misc" }, { ColumnId_Idle2, "Idle 2" }, { ColumnId_Idle3, "Idle 3" }, { ColumnId_Idle4, "Idle 4" }, { ColumnId_Idle5, "Idle 5" }, { ColumnId_Idle6, "Idle 6" }, { ColumnId_Idle7, "Idle 7" }, { ColumnId_Idle8, "Idle 8" }, { ColumnId_Idle9, "Idle 9" }, { ColumnId_RegionWeather, "Weather" }, { ColumnId_WeatherName, "Type" }, { ColumnId_WeatherChance, "Percent Chance" }, { ColumnId_Text, "Text" }, { ColumnId_TextureNickname, "Texture Nickname" }, { ColumnId_PluginIndex, "Plugin Index" }, { ColumnId_TextureIndex, "Texture Index" }, { ColumnId_LandMapLodIndex, "Land map height LOD" }, { ColumnId_LandNormalsIndex, "Land normals" }, { ColumnId_LandHeightsIndex, "Land heights" }, { ColumnId_LandColoursIndex, "Land colors" }, { ColumnId_LandTexturesIndex, "Land textures" }, { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, { ColumnId_UseValue3, "Use value 3" }, { ColumnId_UseValue4, "Use value 4" }, { ColumnId_Attribute1, "Attribute 1" }, { ColumnId_Attribute2, "Attribute 2" }, { ColumnId_MajorSkill1, "Major Skill 1" }, { ColumnId_MajorSkill2, "Major Skill 2" }, { ColumnId_MajorSkill3, "Major Skill 3" }, { ColumnId_MajorSkill4, "Major Skill 4" }, { ColumnId_MajorSkill5, "Major Skill 5" }, { ColumnId_MinorSkill1, "Minor Skill 1" }, { ColumnId_MinorSkill2, "Minor Skill 2" }, { ColumnId_MinorSkill3, "Minor Skill 3" }, { ColumnId_MinorSkill4, "Minor Skill 4" }, { ColumnId_MinorSkill5, "Minor Skill 5" }, { ColumnId_Skill1, "Skill 1" }, { ColumnId_Skill2, "Skill 2" }, { ColumnId_Skill3, "Skill 3" }, { ColumnId_Skill4, "Skill 4" }, { ColumnId_Skill5, "Skill 5" }, { ColumnId_Skill6, "Skill 6" }, { ColumnId_Skill7, "Skill 7" }, { 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) { 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) { 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; } namespace { 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 }; static const char* sSpellTypes[] = { "Spell", "Ability", "Blight", "Disease", "Curse", "Power", 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* 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* 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* sVarTypeEnums[] = { "unknown", "none", "short", "integer", "long", "float", "string", 0 }; static const char* sDialogueTypeEnums[] = { "Topic", "Voice", "Greeting", "Persuasion", 0 }; static const char* sQuestStatusTypes[] = { "None", "Name", "Finished", "Restart", 0 }; static const char* sGenderEnums[] = { "Male", "Female", 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* 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* 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 }; // 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 }; 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) { 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; } } } bool CSMWorld::Columns::hasEnums(ColumnId column) { return getEnumNames(column) != 0 || column == ColumnId_RecordType; } std::vector> CSMWorld::Columns::getEnums(ColumnId column) { std::vector> enums; if (const char** table = getEnumNames(column)) for (int i = 0; table[i]; ++i) enums.emplace_back(i, table[i]); else if (column == ColumnId_BloodType) { for (int i = 0; i < 8; 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) { enums.emplace_back(UniversalId::Type_None, ""); // none for (int i = UniversalId::Type_None + 1; i < UniversalId::NumberOfTypes; ++i) enums.emplace_back(i, UniversalId(static_cast(i)).getTypeName()); } return enums; } openmw-openmw-0.49.0/apps/opencs/model/world/columns.hpp000066400000000000000000000343061503074453300233000ustar00rootroot00000000000000#ifndef CSM_WOLRD_COLUMNS_H #define CSM_WOLRD_COLUMNS_H #include #include #include namespace CSMWorld { namespace Columns { enum ColumnId { ColumnId_Value = 0, ColumnId_Id = 1, ColumnId_Modification = 2, ColumnId_RecordType = 3, ColumnId_ValueType = 4, ColumnId_Description = 5, ColumnId_Specialisation = 6, ColumnId_Attribute = 7, ColumnId_Name = 8, ColumnId_Playable = 9, ColumnId_Hidden = 10, ColumnId_MaleWeight = 11, ColumnId_FemaleWeight = 12, ColumnId_MaleHeight = 13, ColumnId_FemaleHeight = 14, ColumnId_Volume = 15, ColumnId_MinRange = 16, ColumnId_MaxRange = 17, ColumnId_SoundFile = 18, ColumnId_MapColour = 19, ColumnId_SleepEncounter = 20, ColumnId_Texture = 21, ColumnId_SpellType = 22, ColumnId_Cost = 23, ColumnId_ScriptText = 24, ColumnId_Region = 25, ColumnId_Cell = 26, ColumnId_Scale = 27, ColumnId_Owner = 28, ColumnId_Soul = 29, ColumnId_Faction = 30, ColumnId_FactionIndex = 31, ColumnId_Charges = 32, ColumnId_Enchantment = 33, ColumnId_StackCount = 34, ColumnId_Teleport = 35, ColumnId_TeleportCell = 36, ColumnId_LockLevel = 37, ColumnId_Key = 38, ColumnId_Trap = 39, ColumnId_BeastRace = 40, ColumnId_AutoCalc = 41, ColumnId_StarterSpell = 42, ColumnId_AlwaysSucceeds = 43, ColumnId_SleepForbidden = 44, ColumnId_InteriorWater = 45, ColumnId_InteriorSky = 46, ColumnId_Model = 47, ColumnId_Script = 48, ColumnId_Icon = 49, ColumnId_Weight = 50, ColumnId_EnchantmentPoints = 51, ColumnId_Quality = 52, // unused ColumnId_AiHello = 54, ColumnId_AiFlee = 55, ColumnId_AiFight = 56, ColumnId_AiAlarm = 57, ColumnId_BuysWeapons = 58, ColumnId_BuysArmor = 59, ColumnId_BuysClothing = 60, ColumnId_BuysBooks = 61, ColumnId_BuysIngredients = 62, ColumnId_BuysLockpicks = 63, ColumnId_BuysProbes = 64, ColumnId_BuysLights = 65, ColumnId_BuysApparati = 66, ColumnId_BuysRepairItems = 67, ColumnId_BuysMiscItems = 68, ColumnId_BuysPotions = 69, ColumnId_BuysMagicItems = 70, ColumnId_SellsSpells = 71, ColumnId_Trainer = 72, ColumnId_Spellmaking = 73, ColumnId_EnchantingService = 74, ColumnId_RepairService = 75, ColumnId_ApparatusType = 76, ColumnId_ArmorType = 77, ColumnId_Health = 78, ColumnId_ArmorValue = 79, ColumnId_BookType = 80, ColumnId_ClothingType = 81, ColumnId_WeightCapacity = 82, ColumnId_OrganicContainer = 83, ColumnId_Respawn = 84, ColumnId_CreatureType = 85, ColumnId_SoulPoints = 86, ColumnId_ParentCreature = 87, ColumnId_Biped = 88, ColumnId_HasWeapon = 89, // unused ColumnId_Swims = 91, ColumnId_Flies = 92, ColumnId_Walks = 93, ColumnId_Essential = 94, ColumnId_BloodType = 95, // unused ColumnId_OpenSound = 97, ColumnId_CloseSound = 98, ColumnId_Duration = 99, ColumnId_Radius = 100, ColumnId_Colour = 101, ColumnId_Sound = 102, ColumnId_Dynamic = 103, ColumnId_Portable = 104, ColumnId_NegativeLight = 105, ColumnId_EmitterType = 106, // unused (3x) ColumnId_Fire = 110, ColumnId_OffByDefault = 111, ColumnId_IsKey = 112, ColumnId_Race = 113, ColumnId_Class = 114, Columnid_Hair = 115, ColumnId_Head = 116, ColumnId_Female = 117, ColumnId_WeaponType = 118, ColumnId_WeaponSpeed = 119, ColumnId_WeaponReach = 120, ColumnId_MinChop = 121, ColumnId_MaxChip = 122, Columnid_MinSlash = 123, ColumnId_MaxSlash = 124, ColumnId_MinThrust = 125, ColumnId_MaxThrust = 126, ColumnId_Magical = 127, ColumnId_Silver = 128, ColumnId_Filter = 129, ColumnId_PositionXPos = 130, ColumnId_PositionYPos = 131, ColumnId_PositionZPos = 132, ColumnId_PositionXRot = 133, ColumnId_PositionYRot = 134, ColumnId_PositionZRot = 135, ColumnId_DoorPositionXPos = 136, ColumnId_DoorPositionYPos = 137, ColumnId_DoorPositionZPos = 138, ColumnId_DoorPositionXRot = 139, ColumnId_DoorPositionYRot = 140, ColumnId_DoorPositionZRot = 141, ColumnId_DialogueType = 142, ColumnId_QuestIndex = 143, ColumnId_QuestStatusType = 144, ColumnId_QuestDescription = 145, ColumnId_Topic = 146, ColumnId_Journal = 147, ColumnId_Actor = 148, ColumnId_PcFaction = 149, ColumnId_Response = 150, ColumnId_Disposition = 151, ColumnId_Rank = 152, ColumnId_Gender = 153, ColumnId_PcRank = 154, ColumnId_ReferenceableId = 155, ColumnId_ContainerContent = 156, ColumnId_ItemCount = 157, ColumnId_InventoryItemId = 158, ColumnId_CombatState = 159, ColumnId_MagicState = 160, ColumnId_StealthState = 161, ColumnId_EnchantmentType = 162, ColumnId_Vampire = 163, ColumnId_BodyPartType = 164, ColumnId_MeshType = 165, ColumnId_ActorInventory = 166, ColumnId_SpellList = 167, ColumnId_SpellId = 168, ColumnId_NpcDestinations = 169, ColumnId_DestinationCell = 170, ColumnId_PosX = 171, // these are float ColumnId_PosY = 172, // these are float ColumnId_PosZ = 173, // these are float ColumnId_RotX = 174, ColumnId_RotY = 175, ColumnId_RotZ = 176, // unused ColumnId_OwnerGlobal = 178, ColumnId_DefaultProfile = 179, ColumnId_BypassNewGame = 180, ColumnId_GlobalProfile = 181, ColumnId_RefNumCounter = 182, ColumnId_RefNum = 183, ColumnId_Creature = 184, ColumnId_SoundGeneratorType = 185, ColumnId_AllowSpellmaking = 186, ColumnId_AllowEnchanting = 187, ColumnId_BaseCost = 188, ColumnId_School = 189, ColumnId_Particle = 190, ColumnId_CastingObject = 191, ColumnId_HitObject = 192, ColumnId_AreaObject = 193, ColumnId_BoltObject = 194, ColumnId_CastingSound = 195, ColumnId_HitSound = 196, ColumnId_AreaSound = 197, ColumnId_BoltSound = 198, ColumnId_PathgridPoints = 199, ColumnId_PathgridIndex = 200, ColumnId_PathgridPosX = 201, // these are int ColumnId_PathgridPosY = 202, // these are int ColumnId_PathgridPosZ = 203, // these are int ColumnId_PathgridEdges = 204, ColumnId_PathgridEdgeIndex = 205, ColumnId_PathgridEdge0 = 206, ColumnId_PathgridEdge1 = 207, ColumnId_RegionSounds = 208, ColumnId_SoundName = 209, ColumnId_SoundChance = 210, ColumnId_FactionReactions = 211, ColumnId_FactionReaction = 213, ColumnId_EffectList = 214, ColumnId_EffectId = 215, ColumnId_EffectRange = 217, ColumnId_EffectArea = 218, ColumnId_AiPackageList = 219, ColumnId_AiPackageType = 220, ColumnId_AiWanderDist = 221, ColumnId_AiDuration = 222, ColumnId_AiWanderToD = 223, // unused ColumnId_AiWanderRepeat = 225, ColumnId_AiActivateName = 226, // use ColumnId_PosX, etc for AI destinations ColumnId_AiTargetId = 227, ColumnId_AiTargetCell = 228, ColumnId_PartRefList = 229, ColumnId_PartRefType = 230, ColumnId_PartRefMale = 231, ColumnId_PartRefFemale = 232, ColumnId_LevelledList = 233, ColumnId_LevelledItemId = 234, ColumnId_LevelledItemLevel = 235, ColumnId_LevelledItemType = 236, ColumnId_LevelledItemTypeEach = 237, ColumnId_LevelledItemChanceNone = 238, ColumnId_PowerList = 239, ColumnId_Skill = 240, ColumnId_InfoList = 241, ColumnId_InfoCondition = 242, ColumnId_InfoCondFunc = 243, ColumnId_InfoCondVar = 244, ColumnId_InfoCondComp = 245, ColumnId_InfoCondValue = 246, ColumnId_OriginalCell = 247, ColumnId_NpcAttributes = 248, ColumnId_NpcSkills = 249, ColumnId_UChar = 250, ColumnId_NpcMisc = 251, ColumnId_Level = 252, // unused ColumnId_Mana = 255, ColumnId_Fatigue = 256, ColumnId_NpcDisposition = 257, ColumnId_NpcReputation = 258, ColumnId_NpcRank = 259, ColumnId_Gold = 260, // unused ColumnId_RaceAttributes = 262, ColumnId_Male = 263, // unused ColumnId_RaceSkillBonus = 265, // unused ColumnId_RaceBonus = 267, ColumnId_Interior = 268, ColumnId_Ambient = 269, ColumnId_Sunlight = 270, ColumnId_Fog = 271, ColumnId_FogDensity = 272, ColumnId_WaterLevel = 273, ColumnId_MapColor = 274, ColumnId_FileFormat = 275, ColumnId_FileDescription = 276, ColumnId_Author = 277, ColumnId_MinMagnitude = 278, ColumnId_MaxMagnitude = 279, ColumnId_CreatureAttributes = 280, ColumnId_AttributeValue = 281, ColumnId_CreatureAttack = 282, ColumnId_MinAttack = 283, ColumnId_MaxAttack = 284, ColumnId_CreatureMisc = 285, 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, ColumnId_WeatherChance = 296, ColumnId_Text = 297, ColumnId_TextureNickname = 298, ColumnId_PluginIndex = 299, ColumnId_TextureIndex = 300, ColumnId_LandMapLodIndex = 301, ColumnId_LandNormalsIndex = 302, ColumnId_LandHeightsIndex = 303, ColumnId_LandColoursIndex = 304, ColumnId_LandTexturesIndex = 305, ColumnId_RankName = 306, ColumnId_FactionRanks = 307, ColumnId_FactionPrimSkill = 308, ColumnId_FactionFavSkill = 309, ColumnId_FactionRep = 310, 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, ColumnId_UseValue2 = 0x10001, ColumnId_UseValue3 = 0x10002, ColumnId_UseValue4 = 0x10003, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of attributes. Note that this is not the number of different // attributes, but the number of attributes that can be references from a record. ColumnId_Attribute1 = 0x20000, ColumnId_Attribute2 = 0x20001, // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of skills. Note that this is not the number of different // skills, but the number of skills that can be references from a record. ColumnId_MajorSkill1 = 0x30000, ColumnId_MajorSkill2 = 0x30001, ColumnId_MajorSkill3 = 0x30002, ColumnId_MajorSkill4 = 0x30003, ColumnId_MajorSkill5 = 0x30004, ColumnId_MinorSkill1 = 0x40000, ColumnId_MinorSkill2 = 0x40001, ColumnId_MinorSkill3 = 0x40002, ColumnId_MinorSkill4 = 0x40003, ColumnId_MinorSkill5 = 0x40004, ColumnId_Skill1 = 0x50000, ColumnId_Skill2 = 0x50001, ColumnId_Skill3 = 0x50002, ColumnId_Skill4 = 0x50003, ColumnId_Skill5 = 0x50004, ColumnId_Skill6 = 0x50005, ColumnId_Skill7 = 0x50006 }; std::string getName(ColumnId column); int getId(const std::string& name); ///< Will return -1 for an invalid name. bool hasEnums(ColumnId column); std::vector> getEnums(ColumnId column); ///< Returns an empty vector, if \a column isn't an enum type column. } } #endif openmw-openmw-0.49.0/apps/opencs/model/world/commanddispatcher.cpp000066400000000000000000000242551503074453300253020ustar00rootroot00000000000000#include "commanddispatcher.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../doc/document.hpp" #include "commandmacro.hpp" #include "commands.hpp" #include "idtable.hpp" #include "idtableproxymodel.hpp" #include "record.hpp" std::vector CSMWorld::CommandDispatcher::getDeletableRecords() const { std::vector result; IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); int stateColumnIndex = model.findColumnIndex(Columns::ColumnId_Modification); for (std::vector::const_iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { int row = model.getModelIndex(*iter, 0).row(); // check record state RecordBase::State state = static_cast(model.data(model.index(row, stateColumnIndex)).toInt()); if (state == RecordBase::State_Deleted) continue; // check other columns (only relevant for a subset of the tables) int dialogueTypeIndex = model.searchColumnIndex(Columns::ColumnId_DialogueType); if (dialogueTypeIndex != -1) { int type = model.data(model.index(row, dialogueTypeIndex)).toInt(); if (type != ESM::Dialogue::Topic && type != ESM::Dialogue::Journal) continue; } result.push_back(*iter); } return result; } std::vector CSMWorld::CommandDispatcher::getRevertableRecords() const { std::vector result; 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); for (std::vector::const_iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { int row = model.getModelIndex(*iter, 0).row(); // check record state RecordBase::State state = static_cast(model.data(model.index(row, stateColumnIndex)).toInt()); if (state == RecordBase::State_BaseOnly) continue; 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) { } void CSMWorld::CommandDispatcher::setEditLock(bool locked) { mLocked = locked; } void CSMWorld::CommandDispatcher::setSelection(const std::vector& selection) { mSelection = selection; std::sort(mSelection.begin(), mSelection.end(), Misc::StringUtils::CiComp{}); } void CSMWorld::CommandDispatcher::setExtendedTypes(const std::vector& types) { mExtendedTypes = types; } bool CSMWorld::CommandDispatcher::canDelete() const { if (mLocked) return false; return getDeletableRecords().size() != 0; } bool CSMWorld::CommandDispatcher::canRevert() const { if (mLocked) return false; return getRevertableRecords().size() != 0; } std::vector CSMWorld::CommandDispatcher::getExtendedTypes() const { std::vector tables; if (mId == UniversalId::Type_Cells) { tables.push_back(mId); tables.emplace_back(UniversalId::Type_References); /// \todo add other cell-specific types } return tables; } void CSMWorld::CommandDispatcher::executeModify( QAbstractItemModel* model, const QModelIndex& index, const QVariant& new_) { if (mLocked) return; std::unique_ptr modifyCell; int columnId = model->data(index, ColumnBase::Role_ColumnId).toInt(); if (columnId == Columns::ColumnId_PositionXPos || columnId == Columns::ColumnId_PositionYPos) { 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) { IdTableProxyModel* proxy = dynamic_cast(model); 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)); int cellColumn = model2.searchColumnIndex(Columns::ColumnId_Cell); if (cellColumn != -1) { QModelIndex cellIndex = model2.index(row, cellColumn); std::string cellId = model2.data(cellIndex).toString().toUtf8().data(); if (cellId.find('#') != std::string::npos) { // Need to recalculate the cell modifyCell = std::make_unique(model2, row); } } } } auto modifyData = std::make_unique(*model, index, new_); if (modifyCell.get()) { CommandMacro macro(mDocument.getUndoStack()); macro.push(modifyData.release()); macro.push(modifyCell.release()); } else mDocument.getUndoStack().push(modifyData.release()); } void CSMWorld::CommandDispatcher::executeDelete() { if (mLocked) return; std::vector rows = getDeletableRecords(); if (rows.empty()) return; IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); 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) { 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()))); } else mDocument.getUndoStack().push(new CSMWorld::DeleteCommand(model, id)); } } void CSMWorld::CommandDispatcher::executeRevert() { if (mLocked) return; std::vector rows = getRevertableRecords(); if (rows.empty()) return; IdTable& model = dynamic_cast(*mDocument.getData().getTableModel(mId)); 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) { std::string id = model.data(model.getModelIndex(*iter, columnIndex)).toString().toUtf8().constData(); macro.push(new CSMWorld::RevertCommand(model, id)); } } void CSMWorld::CommandDispatcher::executeExtendedDelete() { 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) { if (*iter == mId) executeDelete(); else if (*iter == UniversalId::Type_References) { 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) { const Record& record = collection.getRecord(i); if (record.mState == RecordBase::State_Deleted) continue; if (!std::binary_search(mSelection.begin(), mSelection.end(), record.get().mCell)) continue; macro.push(new CSMWorld::DeleteCommand(model, record.get().mId.getRefIdString())); } } } } void CSMWorld::CommandDispatcher::executeExtendedRevert() { 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) { if (*iter == mId) executeRevert(); else if (*iter == UniversalId::Type_References) { 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) { const Record& record = collection.getRecord(i); if (!std::binary_search(mSelection.begin(), mSelection.end(), record.get().mCell)) continue; macro.push(new CSMWorld::RevertCommand(model, record.get().mId.getRefIdString())); } } } } openmw-openmw-0.49.0/apps/opencs/model/world/commanddispatcher.hpp000066400000000000000000000042261503074453300253030ustar00rootroot00000000000000#ifndef CSM_WOLRD_COMMANDDISPATCHER_H #define CSM_WOLRD_COMMANDDISPATCHER_H #include #include #include #include #include #include #include "universalid.hpp" class QModelIndex; class QAbstractItemModel; namespace CSMDoc { class Document; } namespace CSMWorld { class CommandDispatcher : public QObject { Q_OBJECT bool mLocked; CSMDoc::Document& mDocument; UniversalId mId; std::vector mSelection; std::vector mExtendedTypes; std::vector getDeletableRecords() const; std::vector getRevertableRecords() const; public: 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 setSelection(const std::vector& selection); void setExtendedTypes(const std::vector& types); ///< Set record lists selected by the user for extended operations. bool canDelete() 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; /// 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: void executeDelete(); void executeRevert(); void executeExtendedDelete(); void executeExtendedRevert(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/commandmacro.cpp000066400000000000000000000011061503074453300242430ustar00rootroot00000000000000 #include "commandmacro.hpp" #include #include CSMWorld::CommandMacro::CommandMacro(QUndoStack& undoStack, const QString& description) : mUndoStack(undoStack) , mDescription(description) , mStarted(false) { } CSMWorld::CommandMacro::~CommandMacro() { if (mStarted) mUndoStack.endMacro(); } void CSMWorld::CommandMacro::push(QUndoCommand* command) { if (!mStarted) { mUndoStack.beginMacro(mDescription.isEmpty() ? command->text() : mDescription); mStarted = true; } mUndoStack.push(command); } openmw-openmw-0.49.0/apps/opencs/model/world/commandmacro.hpp000066400000000000000000000012471503074453300242560ustar00rootroot00000000000000#ifndef CSM_WOLRD_COMMANDMACRO_H #define CSM_WOLRD_COMMANDMACRO_H class QUndoStack; class QUndoCommand; #include namespace CSMWorld { class CommandMacro { QUndoStack& mUndoStack; QString mDescription; bool mStarted; /// not implemented CommandMacro(const CommandMacro&); /// not implemented CommandMacro& operator=(const CommandMacro&); public: /// If \a description is empty, the description of the first command is used. CommandMacro(QUndoStack& undoStack, const QString& description = ""); ~CommandMacro(); void push(QUndoCommand* command); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/commands.cpp000066400000000000000000000374241503074453300234200ustar00rootroot00000000000000#include "commands.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cellcoordinates.hpp" #include "idtable.hpp" #include "idtree.hpp" #include "nestedtablewrapper.hpp" #include "pathgrid.hpp" CSMWorld::TouchCommand::TouchCommand(IdTable& table, const std::string& id, QUndoCommand* parent) : QUndoCommand(parent) , mTable(table) , mId(id) , mOld(nullptr) , mChanged(false) { setText(("Touch " + mId).c_str()); } void CSMWorld::TouchCommand::redo() { mOld = mTable.getRecord(mId).clone(); mChanged = mTable.touchRecord(mId); } void CSMWorld::TouchCommand::undo() { if (mChanged) { mTable.setRecord(mId, std::move(mOld)); mChanged = false; } } CSMWorld::ImportLandTexturesCommand::ImportLandTexturesCommand( IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent) : QUndoCommand(parent) , mLands(landTable) , mLtexs(ltexTable) , mOldState(0) { setText("Copy land textures to current plugin"); } void CSMWorld::ImportLandTexturesCommand::redo() { const int pluginColumn = mLands.findColumnIndex(Columns::ColumnId_PluginIndex); const int oldPlugin = mLands.data(mLands.getModelIndex(getOriginId(), pluginColumn)).toInt(); // Original data 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 DataType copy(mOld); // Perform touch/copy/etc... onRedo(); std::unordered_map indexMapping; for (uint16_t index : mOld) { // All indices are offset by 1 for a default texture 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()) { 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 const int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mOldState = mLands.data(mLands.getModelIndex(getDestinationId(), stateColumn)).toInt(); QVariant variant; variant.setValue(copy); mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); } void CSMWorld::ImportLandTexturesCommand::undo() { // Restore to previous int textureColumn = mLands.findColumnIndex(Columns::ColumnId_LandTexturesIndex); QVariant variant; variant.setValue(mOld); mLands.setData(mLands.getModelIndex(getDestinationId(), textureColumn), variant); int stateColumn = mLands.findColumnIndex(Columns::ColumnId_Modification); mLands.setData(mLands.getModelIndex(getDestinationId(), stateColumn), mOldState); // Undo copy/touch/etc... onUndo(); for (auto& ltex : mTouchedTextures) { ESM::RefId id = static_cast*>(ltex.get())->get().mId; mLtexs.setRecord(id.getRefIdString(), std::move(ltex)); } mTouchedTextures.clear(); } 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) { } const std::string& CSMWorld::CopyLandTexturesCommand::getOriginId() const { return mOriginId; } const std::string& CSMWorld::CopyLandTexturesCommand::getDestinationId() const { return mDestId; } 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()); } const std::string& CSMWorld::TouchLandCommand::getOriginId() const { return mId; } const std::string& CSMWorld::TouchLandCommand::getDestinationId() const { return mId; } void CSMWorld::TouchLandCommand::onRedo() { mOld = mLands.getRecord(mId).clone(); mChanged = mLands.touchRecord(mId); } void CSMWorld::TouchLandCommand::onUndo() { if (mChanged) { 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) { if (QAbstractProxyModel* proxy = dynamic_cast(mModel)) { // Replace proxy with actual model 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()); } else { setText("Modify " + mModel->headerData(mIndex.column(), Qt::Horizontal, Qt::DisplayRole).toString()); } // Remember record state before the modification if (CSMWorld::IdTable* table = dynamic_cast(mModel)) { mHasRecordState = true; int stateColumnIndex = table->findColumnIndex(Columns::ColumnId_Modification); int rowIndex = mIndex.row(); if (mIndex.parent().isValid()) { rowIndex = mIndex.parent().row(); } mRecordStateIndex = table->index(rowIndex, stateColumnIndex); mOldRecordState = static_cast(table->data(mRecordStateIndex).toInt()); } mOld = mModel->data(mIndex, Qt::EditRole); mModel->setData(mIndex, mNew); } void CSMWorld::ModifyCommand::undo() { 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(); for (; current != end; ++current) { 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) { setText(("Create record " + id).c_str()); } void CSMWorld::CreateCommand::addValue(int column, const QVariant& value) { mValues[column] = 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) { mType = type; } void CSMWorld::CreateCommand::redo() { 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()); } void CSMWorld::RevertCommand::redo() { 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()); if (state == RecordBase::State_ModifiedOnly) { mModel.removeRows(index.row(), 1); } else { mModel.setData(index, static_cast(RecordBase::State_BaseOnly)); } } void CSMWorld::RevertCommand::undo() { mModel.setRecord(mId, std::move(mOld)); } CSMWorld::DeleteCommand::DeleteCommand( IdTable& model, const std::string& id, CSMWorld::UniversalId::Type type, QUndoCommand* parent) : QUndoCommand(parent) , mModel(model) , mId(id) , mOld(nullptr) , mType(type) { setText(("Delete record " + id).c_str()); } void CSMWorld::DeleteCommand::redo() { 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()); if (state == RecordBase::State_ModifiedOnly) { mModel.removeRows(index.row(), 1); } else { mModel.setData(index, static_cast(RecordBase::State_Deleted)); } } void CSMWorld::DeleteCommand::undo() { mModel.setRecord(mId, std::move(mOld), mType); } 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); } void CSMWorld::ReorderRowsCommand::undo() { int size = static_cast(mNewOrder.size()); std::vector reverse(size); for (int i = 0; i < size; ++i) reverse.at(mNewOrder[i]) = i; 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) { setType(type); setText(("Clone record " + idOrigin + " to the " + idDestination).c_str()); } void CSMWorld::CloneCommand::redo() { 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); } } void CSMWorld::CloneCommand::undo() { mModel.removeRow(mModel.getModelIndex(mId, 0).row()); } 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) : CreateCommand(model, id, parent) { setType(UniversalId::Type_Pathgrid); } void CSMWorld::CreatePathgridCommand::redo() { CreateCommand::redo(); 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(); } mModel.setRecord(mId, std::move(record), mType); } CSMWorld::UpdateCellCommand::UpdateCellCommand(IdTable& model, int row, QUndoCommand* parent) : QUndoCommand(parent) , mModel(model) , mRow(row) { setText("Update cell ID"); } void CSMWorld::UpdateCellCommand::redo() { if (!mNew.isValid()) { int cellColumn = mModel.searchColumnIndex(Columns::ColumnId_Cell); mIndex = mModel.index(mRow, cellColumn); QModelIndex xIndex = mModel.index(mRow, mModel.findColumnIndex(Columns::ColumnId_PositionXPos)); 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); std::ostringstream stream; stream << "#" << x << " " << y; mNew = QString::fromUtf8(stream.str().c_str()); } mModel.setData(mIndex, mNew); } void CSMWorld::UpdateCellCommand::undo() { 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) { 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); } void CSMWorld::DeleteNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand->redo(); mModel.removeRows(mNestedRow, 1, parentIndex); } void CSMWorld::DeleteNestedCommand::undo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.setNestedTable(parentIndex, getOld()); 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) { 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); } void CSMWorld::AddNestedCommand::redo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModifyParentCommand->redo(); mModel.addNestedRow(parentIndex, mNewRow); } void CSMWorld::AddNestedCommand::undo() { QModelIndex parentIndex = mModel.getModelIndex(mId, mParentColumn); mModel.setNestedTable(parentIndex, getOld()); mModifyParentCommand->undo(); } CSMWorld::NestedTableStoring::NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn) : mOld(model.nestedTable(model.getModelIndex(id, parentColumn))) { } CSMWorld::NestedTableStoring::~NestedTableStoring() { delete mOld; } const CSMWorld::NestedTableWrapperBase& CSMWorld::NestedTableStoring::getOld() const { return *mOld; } openmw-openmw-0.49.0/apps/opencs/model/world/commands.hpp000066400000000000000000000213051503074453300234140ustar00rootroot00000000000000#ifndef CSM_WOLRD_COMMANDS_H #define CSM_WOLRD_COMMANDS_H #include "record.hpp" #include #include #include #include #include #include #include #include #include #include "columnimp.hpp" #include "universalid.hpp" namespace CSMWorld { class IdTable; class IdTree; struct NestedTableWrapperBase; class TouchCommand : public QUndoCommand { public: TouchCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); void redo() override; void undo() override; private: IdTable& mTable; std::string mId; std::unique_ptr mOld; bool mChanged; }; /// \brief Adds LandTexture records and modifies texture indices as needed. /// /// LandTexture records are different from other types of records, because /// they only effect the current plugin. Thus, when modifying or copying /// a Land record, all of the LandTexture records referenced need to be /// added to the current plugin. Since these newly added LandTextures could /// have indices that conflict with pre-existing LandTextures in the current /// plugin, the indices might have to be changed, both for the newly added /// LandRecord and within the Land record. class ImportLandTexturesCommand : public QUndoCommand { public: ImportLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, QUndoCommand* parent); void redo() override; void undo() override; protected: using DataType = LandTexturesColumn::DataType; virtual const std::string& getOriginId() const = 0; virtual const std::string& getDestinationId() const = 0; virtual void onRedo() = 0; virtual void onUndo() = 0; IdTable& mLands; IdTable& mLtexs; DataType mOld; int mOldState; std::vector> mTouchedTextures; }; /// \brief This command is used to fix LandTexture records and texture /// indices after cloning a Land. See ImportLandTexturesCommand for /// details. class CopyLandTexturesCommand : public ImportLandTexturesCommand { public: CopyLandTexturesCommand(IdTable& landTable, IdTable& ltexTable, const std::string& origin, const std::string& dest, QUndoCommand* parent = nullptr); private: const std::string& getOriginId() const override; const std::string& getDestinationId() const override; void onRedo() override {} void onUndo() override {} std::string mOriginId; std::string mDestId; }; /// \brief This command brings a land record into the current plugin, adding /// LandTexture records and modifying texture indices as needed. /// \note See ImportLandTextures for more details. class TouchLandCommand : public ImportLandTexturesCommand { public: TouchLandCommand(IdTable& landTable, IdTable& ltexTable, const std::string& id, QUndoCommand* parent = nullptr); private: const std::string& getOriginId() const override; const std::string& getDestinationId() const override; void onRedo() override; void onUndo() override; std::string mId; std::unique_ptr mOld; bool mChanged; }; class ModifyCommand : public QUndoCommand { QAbstractItemModel* mModel; QModelIndex mIndex; QVariant mNew; QVariant mOld; bool mHasRecordState; QModelIndex mRecordStateIndex; CSMWorld::RecordBase::State mOldRecordState; public: ModifyCommand( QAbstractItemModel& model, const QModelIndex& index, const QVariant& new_, QUndoCommand* parent = nullptr); void redo() 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: IdTable& mModel; std::string mId; UniversalId::Type mType; protected: /// Apply modifications set via addValue. void applyModifications(); public: CreateCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); void setType(UniversalId::Type type); void addValue(int column, const QVariant& value); void addNestedValue(int parentColumn, int nestedColumn, const QVariant& value); void redo() override; void undo() override; }; class CloneCommand : public CreateCommand { std::string mIdOrigin; std::vector> mOverrideValues; public: CloneCommand(IdTable& model, const std::string& idOrigin, const std::string& IdDestination, const UniversalId::Type type, QUndoCommand* parent = nullptr); void redo() override; void undo() override; void setOverrideValue(int column, QVariant value); }; class RevertCommand : public QUndoCommand { IdTable& mModel; std::string mId; std::unique_ptr mOld; // not implemented RevertCommand(const RevertCommand&); RevertCommand& operator=(const RevertCommand&); public: RevertCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); ~RevertCommand() override = default; void redo() override; void undo() override; }; class DeleteCommand : public QUndoCommand { IdTable& mModel; std::string mId; std::unique_ptr mOld; UniversalId::Type mType; // 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); ~DeleteCommand() override = default; void redo() override; void undo() override; }; class ReorderRowsCommand : public QUndoCommand { IdTable& mModel; int mBaseIndex; std::vector mNewOrder; public: ReorderRowsCommand(IdTable& model, int baseIndex, const std::vector& newOrder); void redo() override; void undo() override; }; class CreatePathgridCommand : public CreateCommand { public: CreatePathgridCommand(IdTable& model, const std::string& id, QUndoCommand* parent = nullptr); void redo() override; }; /// \brief Update cell ID according to x/y-coordinates /// /// \note The new value will be calculated in the first call to redo instead of the /// constructor to accommodate multiple coordinate-affecting commands being executed /// 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; public: UpdateCellCommand(IdTable& model, int row, QUndoCommand* parent = nullptr); void redo() override; void undo() override; }; class NestedTableStoring { NestedTableWrapperBase* mOld; public: NestedTableStoring(const IdTree& model, const std::string& id, int parentColumn); ~NestedTableStoring(); protected: const NestedTableWrapperBase& getOld() const; }; class DeleteNestedCommand : public QUndoCommand, private NestedTableStoring { IdTree& mModel; std::string mId; int mParentColumn; int mNestedRow; // The command to redo/undo the Modified status of a record ModifyCommand* mModifyParentCommand; public: DeleteNestedCommand( IdTree& model, const std::string& id, int nestedRow, int parentColumn, QUndoCommand* parent = nullptr); void redo() override; void undo() override; }; class AddNestedCommand : public QUndoCommand, private NestedTableStoring { IdTree& mModel; std::string mId; int mNewRow; int mParentColumn; // 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); void redo() override; void undo() override; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/data.cpp000066400000000000000000001731521503074453300225270ustar00rootroot00000000000000#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 "../doc/messages.hpp" #include "columnimp.hpp" #include "columns.hpp" #include "idtable.hpp" #include "idtree.hpp" #include "nestedcoladapterimp.hpp" #include "regionmap.hpp" #include "resourcesmanager.hpp" #include "resourcetable.hpp" namespace CSMWorld { namespace { void removeDialogueInfos( const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection) { const auto topicInfoOrder = infoOrders.find(dialogueId); if (topicInfoOrder == infoOrders.end()) return; 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, &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) { std::vector ids2 = collection.getIds(listDeleted); ids.insert(ids.end(), ids2.begin(), ids2.end()); } int CSMWorld::Data::count(RecordBase::State state, const CollectionBase& collection) { int number = 0; for (int i = 0; i < collection.getSize(); ++i) if (collection.getRecord(i).mState == state) ++number; return number; } 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()) { VFS::registerArchives(mVFS.get(), Files::Collections(mDataPaths), mArchives, true); mResourcesManager.setVFS(mVFS.get()); 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"); 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(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.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Faction, ColumnBase::Display_Faction)); mFactions.getNestableColumn(index)->addColumn( 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.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_RankName, ColumnBase::Display_Rank)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_FactionAttrib1, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_FactionAttrib2, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_FactionPrimSkill, ColumnBase::Display_Integer)); mFactions.getNestableColumn(index)->addColumn( 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)); // Race spells 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)); // 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.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Male, ColumnBase::Display_Integer)); mRaces.getNestableColumn(index)->addColumn( 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.getNestableColumn(index)->addColumn( 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); // Region Weather 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_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.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_SoundName, ColumnBase::Display_Sound)); mRegions.getNestableColumn(index)->addColumn( 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.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)); // Spell effects 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)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mSpells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mSpells.getNestableColumn(index)->addColumn( 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 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.getNestableColumn(index)->addColumn( 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.getNestableColumn(index)->addColumn( 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)); mTopicInfos.getNestableColumn(index)->addColumn( 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(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, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); 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.getNestableColumn(index)->addColumn( 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)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Sunlight, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Fog, ColumnBase::Display_Colour)); mCells.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_FogDensity, ColumnBase::Display_Float)); mCells.getNestableColumn(index)->addColumn( 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)); // Enchantment effects 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)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Skill, ColumnBase::Display_EffectSkill)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Attribute, ColumnBase::Display_EffectAttribute)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_EffectRange, ColumnBase::Display_EffectRange)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_EffectArea, ColumnBase::Display_String)); mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_Duration, ColumnBase::Display_Integer)); // reuse from light mEnchantments.getNestableColumn(index)->addColumn( new NestedChildColumn(Columns::ColumnId_MinMagnitude, ColumnBase::Display_Integer)); mEnchantments.getNestableColumn(index)->addColumn( 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 GenderNpcColumn); 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 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; // new object deleted in dtor of NestedCollection 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_PathgridPosX, 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)); 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)); 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 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) delete *iter; } std::shared_ptr CSMWorld::Data::getResourceSystem() { return mResourceSystem; } std::shared_ptr CSMWorld::Data::getResourceSystem() const { return mResourceSystem; } const CSMWorld::IdCollection& CSMWorld::Data::getGlobals() const { return mGlobals; } CSMWorld::IdCollection& CSMWorld::Data::getGlobals() { return mGlobals; } const CSMWorld::IdCollection& CSMWorld::Data::getGmsts() const { return mGmsts; } CSMWorld::IdCollection& CSMWorld::Data::getGmsts() { return mGmsts; } const CSMWorld::IdCollection& CSMWorld::Data::getSkills() const { return mSkills; } CSMWorld::IdCollection& CSMWorld::Data::getSkills() { return mSkills; } const CSMWorld::IdCollection& CSMWorld::Data::getClasses() const { return mClasses; } CSMWorld::IdCollection& CSMWorld::Data::getClasses() { return mClasses; } const CSMWorld::IdCollection& CSMWorld::Data::getFactions() const { return mFactions; } CSMWorld::IdCollection& CSMWorld::Data::getFactions() { return mFactions; } const CSMWorld::IdCollection& CSMWorld::Data::getRaces() const { return mRaces; } CSMWorld::IdCollection& CSMWorld::Data::getRaces() { return mRaces; } const CSMWorld::IdCollection& CSMWorld::Data::getSounds() const { return mSounds; } CSMWorld::IdCollection& CSMWorld::Data::getSounds() { return mSounds; } const CSMWorld::IdCollection& CSMWorld::Data::getScripts() const { return mScripts; } CSMWorld::IdCollection& CSMWorld::Data::getScripts() { return mScripts; } const CSMWorld::IdCollection& CSMWorld::Data::getRegions() const { return mRegions; } CSMWorld::IdCollection& CSMWorld::Data::getRegions() { return mRegions; } const CSMWorld::IdCollection& CSMWorld::Data::getBirthsigns() const { return mBirthsigns; } CSMWorld::IdCollection& CSMWorld::Data::getBirthsigns() { return mBirthsigns; } const CSMWorld::IdCollection& CSMWorld::Data::getSpells() const { return mSpells; } CSMWorld::IdCollection& CSMWorld::Data::getSpells() { return mSpells; } const CSMWorld::IdCollection& CSMWorld::Data::getTopics() const { return mTopics; } CSMWorld::IdCollection& CSMWorld::Data::getTopics() { return mTopics; } const CSMWorld::IdCollection& CSMWorld::Data::getJournals() const { return mJournals; } CSMWorld::IdCollection& CSMWorld::Data::getJournals() { return mJournals; } const CSMWorld::InfoCollection& CSMWorld::Data::getTopicInfos() const { return mTopicInfos; } CSMWorld::InfoCollection& CSMWorld::Data::getTopicInfos() { return mTopicInfos; } const CSMWorld::InfoCollection& CSMWorld::Data::getJournalInfos() const { return mJournalInfos; } CSMWorld::InfoCollection& CSMWorld::Data::getJournalInfos() { return mJournalInfos; } const CSMWorld::IdCollection& CSMWorld::Data::getCells() const { return mCells; } CSMWorld::IdCollection& CSMWorld::Data::getCells() { return mCells; } const CSMWorld::RefIdCollection& CSMWorld::Data::getReferenceables() const { return mReferenceables; } CSMWorld::RefIdCollection& CSMWorld::Data::getReferenceables() { return mReferenceables; } const CSMWorld::RefCollection& CSMWorld::Data::getReferences() const { return mRefs; } CSMWorld::RefCollection& CSMWorld::Data::getReferences() { return mRefs; } const CSMWorld::IdCollection& CSMWorld::Data::getFilters() const { return mFilters; } CSMWorld::IdCollection& CSMWorld::Data::getFilters() { return mFilters; } const CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() const { return mEnchantments; } CSMWorld::IdCollection& CSMWorld::Data::getEnchantments() { return mEnchantments; } const CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() const { return mBodyParts; } CSMWorld::IdCollection& CSMWorld::Data::getBodyParts() { return mBodyParts; } const CSMWorld::IdCollection& CSMWorld::Data::getDebugProfiles() const { return mDebugProfiles; } 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; } CSMWorld::IdCollection& CSMWorld::Data::getLand() { return mLand; } const CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() const { return mLandTextures; } CSMWorld::IdCollection& CSMWorld::Data::getLandTextures() { return mLandTextures; } const CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() const { return mSoundGens; } CSMWorld::IdCollection& CSMWorld::Data::getSoundGens() { return mSoundGens; } const CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() const { return mMagicEffects; } CSMWorld::IdCollection& CSMWorld::Data::getMagicEffects() { return mMagicEffects; } const CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() const { return mPathgrids; } CSMWorld::SubCellCollection& CSMWorld::Data::getPathgrids() { return mPathgrids; } const CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() const { return mStartScripts; } CSMWorld::IdCollection& CSMWorld::Data::getStartScripts() { return mStartScripts; } const CSMWorld::Resources& CSMWorld::Data::getResources(const UniversalId& id) const { return mResourcesManager.get(id.getType()); } const CSMWorld::MetaData& CSMWorld::Data::getMetaData() const { return mMetaData.getRecord(0).get(); } void CSMWorld::Data::setMetaData(const MetaData& metaData) { mMetaData.setRecord( 0, std::make_unique>(Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } QAbstractItemModel* CSMWorld::Data::getTableModel(const CSMWorld::UniversalId& id) { std::map::iterator iter = mModelIndex.find(id.getType()); 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) { 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()); } return iter->second; } const CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter() const { return mActorAdapter.get(); } CSMWorld::ActorAdapter* CSMWorld::Data::getActorAdapter() { return mActorAdapter.get(); } void CSMWorld::Data::merge() { mGlobals.merge(); } int CSMWorld::Data::getTotalRecords(const std::vector& files) { int records = 0; std::unique_ptr reader = std::make_unique(); for (const auto& file : files) { if (!std::filesystem::exists(file)) continue; 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; if (!mProject && !mBase) { MetaData metaData; metaData.mId = ESM::RefId::stringRefId("sys::meta"); metaData.load(*reader); mMetaData.setRecord(0, std::make_unique>(Record(RecordBase::State_ModifiedOnly, nullptr, &metaData))); } 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& [id, model] : staticMarkers) { const ESM::RefId refId = ESM::RefId::stringRefId(id); if (mReferenceables.searchId(refId) == -1) { ESM::Static newMarker; 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& [id, model] : doorMarkers) { const ESM::RefId refId = ESM::RefId::stringRefId(id); if (mReferenceables.searchId(refId) == -1) { ESM::Door newMarker; 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) { 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 (!reader->hasMoreRecs()) { mDialogue = nullptr; loadFallbackEntries(); return true; } ESM::NAME n = reader->getRecName(); reader->getRecHeader(); bool unhandledRecord = false; switch (n.toInt()) { 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(*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; } mRefs.load(*reader, index, mBase, mRefLoadCache[mCells.getId(index)], messages); 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(*reader, mBase, UniversalId::Type_CreatureLevelledList); break; case ESM::REC_LEVI: 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(*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(*reader, isDeleted); if (isDeleted) { // record vector can be shuffled around which would make pointer to record invalid mDialogue = nullptr; if (mJournals.tryDelete(record.mId)) { removeDialogueInfos(record.mId, mJournalInfoOrder, mJournalInfos); } else if (mTopics.tryDelete(record.mId)) { removeDialogueInfos(record.mId, mTopicInfoOrder, mTopicInfos); } else { 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(); } else { mTopics.load(record, mBase); mDialogue = &mTopics.getRecord(record.mId).get(); } } break; } case ESM::REC_INFO: { if (!mDialogue) { messages.add(UniversalId::Type_None, "Found info record not following a dialogue record", "", CSMDoc::Message::Severity_Error); reader->skipRecord(); break; } if (mDialogue->mType == ESM::Dialogue::Journal) mJournalInfos.load(*reader, mBase, *mDialogue, mJournalInfoOrder); else mTopicInfos.load(*reader, mBase, *mDialogue, mTopicInfoOrder); break; } case ESM::REC_FILT: if (!mProject) { unhandledRecord = true; break; } mFilters.load(*reader, mBase); break; case ESM::REC_DBGP: if (!mProject) { unhandledRecord = true; break; } mDebugProfiles.load(*reader, mBase); break; case ESM::REC_SELG: if (!mProject) { unhandledRecord = true; break; } mSelectionGroups.load(*reader, mBase); break; default: unhandledRecord = true; } if (unhandledRecord) { messages.add( UniversalId::Type_None, "Unsupported record type: " + n.toString(), "", CSMDoc::Message::Severity_Error); reader->skipRecord(); } return false; } 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) + count(state, mTopics) + count(state, mTopicInfos) + count(state, mJournals) + count(state, mJournalInfos); } 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; } void CSMWorld::Data::assetsChanged() { mVFS.get()->reset(); 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 }; size_t numAssetTables = sizeof(assetTableIds) / sizeof(UniversalId); for (size_t i = 0; i < numAssetTables; ++i) { ResourceTable* table = static_cast(getTableModel(assetTableIds[i])); table->beginReset(); } // Trigger recreation mResourcesManager.recreateResources(); for (size_t i = 0; i < numAssetTables; ++i) { ResourceTable* table = static_cast(getTableModel(assetTableIds[i])); table->endReset(); } // Get rid of potentially old cached assets mResourceSystem->clearCache(); emit assetTablesChanged(); } void CSMWorld::Data::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (topLeft.column() <= 0) emit idListChanged(); } void CSMWorld::Data::rowsChanged(const QModelIndex& parent, int start, int end) { emit idListChanged(); } const VFS::Manager* CSMWorld::Data::getVFS() const { return mVFS.get(); } openmw-openmw-0.49.0/apps/opencs/model/world/data.hpp000066400000000000000000000244301503074453300225260ustar00rootroot00000000000000#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 "cell.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 "universalid.hpp" #ifndef Q_MOC_RUN #include "subcellcollection.hpp" #endif class QAbstractItemModel; class QModelIndex; namespace Resource { class ResourceSystem; } namespace VFS { class Manager; } namespace ESM { class ESMReader; } namespace CSMDoc { class Messages; } namespace CSMWorld { class ActorAdapter; class CollectionBase; class Resources; class Data : public QObject { 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; Data(const Data&) = delete; Data& operator=(const Data&) = delete; void addModel(QAbstractItemModel* model, UniversalId::Type type, bool update = true); static void appendIds(std::vector& ids, const CollectionBase& collection, bool listDeleted); ///< Append all IDs from collection to \a ids. static int count(RecordBase::State state, const CollectionBase& collection); void loadFallbackEntries(); public: Data(ToUTF8::FromType encoding, const Files::PathContainer& dataPaths, const std::vector& archives, const std::filesystem::path& resDir); ~Data() override; const VFS::Manager* getVFS() const; std::shared_ptr getResourceSystem(); std::shared_ptr getResourceSystem() const; const IdCollection& getGlobals() const; IdCollection& getGlobals(); const IdCollection& getGmsts() const; IdCollection& getGmsts(); const IdCollection& getSkills() const; IdCollection& getSkills(); const IdCollection& getClasses() const; IdCollection& getClasses(); const IdCollection& getFactions() const; IdCollection& getFactions(); const IdCollection& getRaces() const; IdCollection& getRaces(); const IdCollection& getSounds() const; IdCollection& getSounds(); const IdCollection& getScripts() const; IdCollection& getScripts(); const IdCollection& getRegions() const; IdCollection& getRegions(); const IdCollection& getBirthsigns() const; IdCollection& getBirthsigns(); const IdCollection& getSpells() const; IdCollection& getSpells(); const IdCollection& getTopics() const; IdCollection& getTopics(); const IdCollection& getJournals() const; IdCollection& getJournals(); const InfoCollection& getTopicInfos() const; InfoCollection& getTopicInfos(); const InfoCollection& getJournalInfos() const; InfoCollection& getJournalInfos(); const IdCollection& getCells() const; IdCollection& getCells(); const RefIdCollection& getReferenceables() const; RefIdCollection& getReferenceables(); const RefCollection& getReferences() const; RefCollection& getReferences(); const IdCollection& getFilters() const; IdCollection& getFilters(); const IdCollection& getEnchantments() const; IdCollection& getEnchantments(); const IdCollection& getBodyParts() const; IdCollection& getBodyParts(); const IdCollection& getDebugProfiles() const; IdCollection& getDebugProfiles(); const IdCollection& getSelectionGroups() const; IdCollection& getSelectionGroups(); const IdCollection& getLand() const; IdCollection& getLand(); const IdCollection& getLandTextures() const; IdCollection& getLandTextures(); const IdCollection& getSoundGens() const; IdCollection& getSoundGens(); const IdCollection& getMagicEffects() const; IdCollection& getMagicEffects(); const SubCellCollection& getPathgrids() const; SubCellCollection& getPathgrids(); const IdCollection& getStartScripts() const; IdCollection& getStartScripts(); /// Throws an exception, if \a id does not match a resources list. const Resources& getResources(const UniversalId& id) const; const MetaData& getMetaData() const; void setMetaData(const MetaData& metaData); 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 ActorAdapter* getActorAdapter() const; ActorAdapter* getActorAdapter(); void merge(); ///< Merge modified into base. int getTotalRecords(const std::vector& files); // for better loading bar 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 bool continueLoading(CSMDoc::Messages& messages); ///< \return Finished? void finishLoading(); 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 int count(RecordBase::State state) const; ///< Return number of top-level records with the given \a state. signals: void idListChanged(); void assetTablesChanged(); public slots: void assetsChanged(); private slots: void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsChanged(const QModelIndex& parent, int start, int end); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/defaultgmsts.cpp000066400000000000000000001657161503074453300243270ustar00rootroot00000000000000#include "defaultgmsts.hpp" #include const float FInf = std::numeric_limits::infinity(); 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] = { "fAIFleeFleeMult", "fAIFleeHealthMult", "fAIMagicSpellMult", "fAIMeleeArmorMult", "fAIMeleeSummWeaponMult", "fAIMeleeWeaponMult", "fAIRangeMagicSpellMult", "fAIRangeMeleeWeaponMult", "fAlarmRadius", "fAthleticsRunBonus", "fAudioDefaultMaxDistance", "fAudioDefaultMinDistance", "fAudioMaxDistanceMult", "fAudioMinDistanceMult", "fAudioVoiceDefaultMaxDistance", "fAudioVoiceDefaultMinDistance", "fAutoPCSpellChance", "fAutoSpellChance", "fBargainOfferBase", "fBargainOfferMulti", "fBarterGoldResetDelay", "fBaseRunMultiplier", "fBlockStillBonus", "fBribe1000Mod", "fBribe100Mod", "fBribe10Mod", "fCombatAngleXY", "fCombatAngleZ", "fCombatArmorMinMult", "fCombatBlockLeftAngle", "fCombatBlockRightAngle", "fCombatCriticalStrikeMult", "fCombatDelayCreature", "fCombatDelayNPC", "fCombatDistance", "fCombatDistanceWerewolfMod", "fCombatForceSideAngle", "fCombatInvisoMult", "fCombatKODamageMult", "fCombatTorsoSideAngle", "fCombatTorsoStartPercent", "fCombatTorsoStopPercent", "fConstantEffectMult", "fCorpseClearDelay", "fCorpseRespawnDelay", "fCrimeGoldDiscountMult", "fCrimeGoldTurnInMult", "fCrimeStealing", "fDamageStrengthBase", "fDamageStrengthMult", "fDifficultyMult", "fDiseaseXferChance", "fDispAttacking", "fDispBargainFailMod", "fDispBargainSuccessMod", "fDispCrimeMod", "fDispDiseaseMod", "fDispFactionMod", "fDispFactionRankBase", "fDispFactionRankMult", "fDispositionMod", "fDispPersonalityBase", "fDispPersonalityMult", "fDispPickPocketMod", "fDispRaceMod", "fDispStealing", "fDispWeaponDrawn", "fEffectCostMult", "fElementalShieldMult", "fEnchantmentChanceMult", "fEnchantmentConstantChanceMult", "fEnchantmentConstantDurationMult", "fEnchantmentMult", "fEnchantmentValueMult", "fEncumberedMoveEffect", "fEncumbranceStrMult", "fEndFatigueMult", "fFallAcroBase", "fFallAcroMult", "fFallDamageDistanceMin", "fFallDistanceBase", "fFallDistanceMult", "fFatigueAttackBase", "fFatigueAttackMult", "fFatigueBase", "fFatigueBlockBase", "fFatigueBlockMult", "fFatigueJumpBase", "fFatigueJumpMult", "fFatigueMult", "fFatigueReturnBase", "fFatigueReturnMult", "fFatigueRunBase", "fFatigueRunMult", "fFatigueSneakBase", "fFatigueSneakMult", "fFatigueSpellBase", "fFatigueSpellCostMult", "fFatigueSpellMult", "fFatigueSwimRunBase", "fFatigueSwimRunMult", "fFatigueSwimWalkBase", "fFatigueSwimWalkMult", "fFightDispMult", "fFightDistanceMultiplier", "fFightStealing", "fFleeDistance", "fGreetDistanceReset", "fHandtoHandHealthPer", "fHandToHandReach", "fHoldBreathEndMult", "fHoldBreathTime", "fIdleChanceMultiplier", "fIngredientMult", "fInteriorHeadTrackMult", "fJumpAcrobaticsBase", "fJumpAcroMultiplier", "fJumpEncumbranceBase", "fJumpEncumbranceMultiplier", "fJumpMoveBase", "fJumpMoveMult", "fJumpRunMultiplier", "fKnockDownMult", "fLevelMod", "fLevelUpHealthEndMult", "fLightMaxMod", "fLuckMod", "fMagesGuildTravel", "fMagicCreatureCastDelay", "fMagicDetectRefreshRate", "fMagicItemConstantMult", "fMagicItemCostMult", "fMagicItemOnceMult", "fMagicItemPriceMult", "fMagicItemRechargePerSecond", "fMagicItemStrikeMult", "fMagicItemUsedMult", "fMagicStartIconBlink", "fMagicSunBlockedMult", "fMajorSkillBonus", "fMaxFlySpeed", "fMaxHandToHandMult", "fMaxHeadTrackDistance", "fMaxWalkSpeed", "fMaxWalkSpeedCreature", "fMedMaxMod", "fMessageTimePerChar", "fMinFlySpeed", "fMinHandToHandMult", "fMinorSkillBonus", "fMinWalkSpeed", "fMinWalkSpeedCreature", "fMiscSkillBonus", "fNPCbaseMagickaMult", "fNPCHealthBarFade", "fNPCHealthBarTime", "fPCbaseMagickaMult", "fPerDieRollMult", "fPersonalityMod", "fPerTempMult", "fPickLockMult", "fPickPocketMod", "fPotionMinUsefulDuration", "fPotionStrengthMult", "fPotionT1DurMult", "fPotionT1MagMult", "fPotionT4BaseStrengthMult", "fPotionT4EquipStrengthMult", "fProjectileMaxSpeed", "fProjectileMinSpeed", "fProjectileThrownStoreChance", "fRepairAmountMult", "fRepairMult", "fReputationMod", "fRestMagicMult", "fSeriousWoundMult", "fSleepRandMod", "fSleepRestMod", "fSneakBootMult", "fSneakDistanceBase", "fSneakDistanceMultiplier", "fSneakNoViewMult", "fSneakSkillMult", "fSneakSpeedMultiplier", "fSneakUseDelay", "fSneakUseDist", "fSneakViewMult", "fSoulGemMult", "fSpecialSkillBonus", "fSpellMakingValueMult", "fSpellPriceMult", "fSpellValueMult", "fStromWalkMult", "fStromWindSpeed", "fSuffocationDamage", "fSwimHeightScale", "fSwimRunAthleticsMult", "fSwimRunBase", "fSwimWalkAthleticsMult", "fSwimWalkBase", "fSwingBlockBase", "fSwingBlockMult", "fTargetSpellMaxSpeed", "fThrownWeaponMaxSpeed", "fThrownWeaponMinSpeed", "fTrapCostMult", "fTravelMult", "fTravelTimeMult", "fUnarmoredBase1", "fUnarmoredBase2", "fVanityDelay", "fVoiceIdleOdds", "fWaterReflectUpdateAlways", "fWaterReflectUpdateSeldom", "fWeaponDamageMult", "fWeaponFatigueBlockMult", "fWeaponFatigueMult", "fWereWolfAcrobatics", "fWereWolfAgility", "fWereWolfAlchemy", "fWereWolfAlteration", "fWereWolfArmorer", "fWereWolfAthletics", "fWereWolfAxe", "fWereWolfBlock", "fWereWolfBluntWeapon", "fWereWolfConjuration", "fWereWolfDestruction", "fWereWolfEnchant", "fWereWolfEndurance", "fWereWolfFatigue", "fWereWolfHandtoHand", "fWereWolfHealth", "fWereWolfHeavyArmor", "fWereWolfIllusion", "fWereWolfIntellegence", "fWereWolfLightArmor", "fWereWolfLongBlade", "fWereWolfLuck", "fWereWolfMagicka", "fWereWolfMarksman", "fWereWolfMediumArmor", "fWereWolfMerchantile", "fWereWolfMysticism", "fWereWolfPersonality", "fWereWolfRestoration", "fWereWolfRunMult", "fWereWolfSecurity", "fWereWolfShortBlade", "fWereWolfSilverWeaponDamageMult", "fWereWolfSneak", "fWereWolfSpear", "fWereWolfSpeechcraft", "fWereWolfSpeed", "fWereWolfStrength", "fWereWolfUnarmored", "fWereWolfWillPower", "fWortChanceValue", }; const char* CSMWorld::DefaultGmsts::Ints[CSMWorld::DefaultGmsts::IntCount] = { "i1stPersonSneakDelta", "iAlarmAttack", "iAlarmKilling", "iAlarmPickPocket", "iAlarmStealing", "iAlarmTresspass", "iAlchemyMod", "iAutoPCSpellMax", "iAutoRepFacMod", "iAutoRepLevMod", "iAutoSpellAlterationMax", "iAutoSpellAttSkillMin", "iAutoSpellConjurationMax", "iAutoSpellDestructionMax", "iAutoSpellIllusionMax", "iAutoSpellMysticismMax", "iAutoSpellRestorationMax", "iAutoSpellTimesCanCast", "iBarterFailDisposition", "iBarterSuccessDisposition", "iBaseArmorSkill", "iBlockMaxChance", "iBlockMinChance", "iBootsWeight", "iCrimeAttack", "iCrimeKilling", "iCrimePickPocket", "iCrimeThreshold", "iCrimeThresholdMultiplier", "iCrimeTresspass", "iCuirassWeight", "iDaysinPrisonMod", "iDispAttackMod", "iDispKilling", "iDispTresspass", "iFightAlarmMult", "iFightAttack", "iFightAttacking", "iFightDistanceBase", "iFightKilling", "iFightPickpocket", "iFightTrespass", "iFlee", "iGauntletWeight", "iGreavesWeight", "iGreetDistanceMultiplier", "iGreetDuration", "iHelmWeight", "iKnockDownOddsBase", "iKnockDownOddsMult", "iLevelUp01Mult", "iLevelUp02Mult", "iLevelUp03Mult", "iLevelUp04Mult", "iLevelUp05Mult", "iLevelUp06Mult", "iLevelUp07Mult", "iLevelUp08Mult", "iLevelUp09Mult", "iLevelUp10Mult", "iLevelupMajorMult", "iLevelupMajorMultAttribute", "iLevelupMinorMult", "iLevelupMinorMultAttribute", "iLevelupMiscMultAttriubte", "iLevelupSpecialization", "iLevelupTotal", "iMagicItemChargeConst", "iMagicItemChargeOnce", "iMagicItemChargeStrike", "iMagicItemChargeUse", "iMaxActivateDist", "iMaxInfoDist", "iMonthsToRespawn", "iNumberCreatures", "iPauldronWeight", "iPerMinChance", "iPerMinChange", "iPickMaxChance", "iPickMinChance", "iShieldWeight", "iSoulAmountForConstantEffect", "iTrainingMod", "iVoiceAttackOdds", "iVoiceHitOdds", "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", "iWereWolfLevelToAttack", }; const char* CSMWorld::DefaultGmsts::Strings[CSMWorld::DefaultGmsts::StringCount] = { "s3dAudio", "s3dHardware", "s3dSoftware", "sAbsorb", "sAcrobat", "sActivate", "sActivateXbox", "sActorInCombat", "sAdmire", "sAdmireFail", "sAdmireSuccess", "sAgent", "sAgiDesc", "sAIDistance", "sAlembic", "sAllTab", "sAlways", "sAlways_Run", "sand", "sApparatus", "sApparelTab", "sArcher", "sArea", "sAreaDes", "sArmor", "sArmorRating", "sAsk", "sAssassin", "sAt", "sAttack", "sAttributeAgility", "sAttributeEndurance", "sAttributeIntelligence", "sAttributeListTitle", "sAttributeLuck", "sAttributePersonality", "sAttributesMenu1", "sAttributeSpeed", "sAttributeStrength", "sAttributeWillpower", "sAudio", "sAuto_Run", "sBack", "sBackspace", "sBackXbox", "sBarbarian", "sBard", "sBarter", "sBarterDialog1", "sBarterDialog10", "sBarterDialog11", "sBarterDialog12", "sBarterDialog2", "sBarterDialog3", "sBarterDialog4", "sBarterDialog5", "sBarterDialog6", "sBarterDialog7", "sBarterDialog8", "sBarterDialog9", "sBattlemage", "sBestAttack", "sBirthSign", "sBirthsignmenu1", "sBirthsignmenu2", "sBlocks", "sBonusSkillTitle", "sBookPageOne", "sBookPageTwo", "sBookSkillMessage", "sBounty", "sBreath", "sBribe 10 Gold", "sBribe 100 Gold", "sBribe 1000 Gold", "sBribeFail", "sBribeSuccess", "sBuy", "sBye", "sCalcinator", "sCancel", "sCantEquipWeapWarning", "sCastCost", "sCaughtStealingMessage", "sCenter", "sChangedMastersMsg", "sCharges", "sChooseClassMenu1", "sChooseClassMenu2", "sChooseClassMenu3", "sChooseClassMenu4", "sChop", "sClass", "sClassChoiceMenu1", "sClassChoiceMenu2", "sClassChoiceMenu3", "sClose", "sCompanionShare", "sCompanionWarningButtonOne", "sCompanionWarningButtonTwo", "sCompanionWarningMessage", "sCondition", "sConsoleTitle", "sContainer", "sContentsMessage1", "sContentsMessage2", "sContentsMessage3", "sControlerVibration", "sControls", "sControlsMenu1", "sControlsMenu2", "sControlsMenu3", "sControlsMenu4", "sControlsMenu5", "sControlsMenu6", "sCostChance", "sCostCharge", "sCreate", "sCreateClassMenu1", "sCreateClassMenu2", "sCreateClassMenu3", "sCreateClassMenuHelp1", "sCreateClassMenuHelp2", "sCreateClassMenuWarning", "sCreatedEffects", "sCrimeHelp", "sCrimeMessage", "sCrouch_Sneak", "sCrouchXbox", "sCrusader", "sCursorOff", "sCustom", "sCustomClassName", "sDamage", "sDark_Gamma", "sDay", "sDefaultCellname", "sDelete", "sDeleteGame", "sDeleteNote", "sDeleteSpell", "sDeleteSpellError", "sDetail_Level", "sDialogMenu1", "sDialogText1Xbox", "sDialogText2Xbox", "sDialogText3Xbox", "sDifficulty", "sDisposeCorpseFail", "sDisposeofCorpse", "sDone", "sDoYouWantTo", "sDrain", "sDrop", "sDuration", "sDurationDes", "sEasy", "sEditNote", "sEffectAbsorbAttribute", "sEffectAbsorbFatigue", "sEffectAbsorbHealth", "sEffectAbsorbSkill", "sEffectAbsorbSpellPoints", "sEffectAlmsiviIntervention", "sEffectBlind", "sEffectBoundBattleAxe", "sEffectBoundBoots", "sEffectBoundCuirass", "sEffectBoundDagger", "sEffectBoundGloves", "sEffectBoundHelm", "sEffectBoundLongbow", "sEffectBoundLongsword", "sEffectBoundMace", "sEffectBoundShield", "sEffectBoundSpear", "sEffectBurden", "sEffectCalmCreature", "sEffectCalmHumanoid", "sEffectChameleon", "sEffectCharm", "sEffectCommandCreatures", "sEffectCommandHumanoids", "sEffectCorpus", "sEffectCureBlightDisease", "sEffectCureCommonDisease", "sEffectCureCorprusDisease", "sEffectCureParalyzation", "sEffectCurePoison", "sEffectDamageAttribute", "sEffectDamageFatigue", "sEffectDamageHealth", "sEffectDamageMagicka", "sEffectDamageSkill", "sEffectDemoralizeCreature", "sEffectDemoralizeHumanoid", "sEffectDetectAnimal", "sEffectDetectEnchantment", "sEffectDetectKey", "sEffectDisintegrateArmor", "sEffectDisintegrateWeapon", "sEffectDispel", "sEffectDivineIntervention", "sEffectDrainAttribute", "sEffectDrainFatigue", "sEffectDrainHealth", "sEffectDrainSkill", "sEffectDrainSpellpoints", "sEffectExtraSpell", "sEffectFeather", "sEffectFireDamage", "sEffectFireShield", "sEffectFortifyAttackBonus", "sEffectFortifyAttribute", "sEffectFortifyFatigue", "sEffectFortifyHealth", "sEffectFortifyMagickaMultiplier", "sEffectFortifySkill", "sEffectFortifySpellpoints", "sEffectFrenzyCreature", "sEffectFrenzyHumanoid", "sEffectFrostDamage", "sEffectFrostShield", "sEffectInvisibility", "sEffectJump", "sEffectLevitate", "sEffectLight", "sEffectLightningShield", "sEffectLock", "sEffectMark", "sEffectNightEye", "sEffectOpen", "sEffectParalyze", "sEffectPoison", "sEffectRallyCreature", "sEffectRallyHumanoid", "sEffectRecall", "sEffectReflect", "sEffectRemoveCurse", "sEffectResistBlightDisease", "sEffectResistCommonDisease", "sEffectResistCorprusDisease", "sEffectResistFire", "sEffectResistFrost", "sEffectResistMagicka", "sEffectResistNormalWeapons", "sEffectResistParalysis", "sEffectResistPoison", "sEffectResistShock", "sEffectRestoreAttribute", "sEffectRestoreFatigue", "sEffectRestoreHealth", "sEffectRestoreSkill", "sEffectRestoreSpellPoints", "sEffects", "sEffectSanctuary", "sEffectShield", "sEffectShockDamage", "sEffectSilence", "sEffectSlowFall", "sEffectSoultrap", "sEffectSound", "sEffectSpellAbsorption", "sEffectStuntedMagicka", "sEffectSummonAncestralGhost", "sEffectSummonBonelord", "sEffectSummonCenturionSphere", "sEffectSummonClannfear", "sEffectSummonCreature01", "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", "sEffectSummonCreature05", "sEffectSummonDaedroth", "sEffectSummonDremora", "sEffectSummonFabricant", "sEffectSummonFlameAtronach", "sEffectSummonFrostAtronach", "sEffectSummonGoldensaint", "sEffectSummonGreaterBonewalker", "sEffectSummonHunger", "sEffectSummonLeastBonewalker", "sEffectSummonScamp", "sEffectSummonSkeletalMinion", "sEffectSummonStormAtronach", "sEffectSummonWingedTwilight", "sEffectSunDamage", "sEffectSwiftSwim", "sEffectTelekinesis", "sEffectTurnUndead", "sEffectVampirism", "sEffectWaterBreathing", "sEffectWaterWalking", "sEffectWeaknessToBlightDisease", "sEffectWeaknessToCommonDisease", "sEffectWeaknessToCorprusDisease", "sEffectWeaknessToFire", "sEffectWeaknessToFrost", "sEffectWeaknessToMagicka", "sEffectWeaknessToNormalWeapons", "sEffectWeaknessToPoison", "sEffectWeaknessToShock", "sEnableJoystick", "sEnchanting", "sEnchantItems", "sEnchantmentHelp1", "sEnchantmentHelp10", "sEnchantmentHelp2", "sEnchantmentHelp3", "sEnchantmentHelp4", "sEnchantmentHelp5", "sEnchantmentHelp6", "sEnchantmentHelp7", "sEnchantmentHelp8", "sEnchantmentHelp9", "sEnchantmentMenu1", "sEnchantmentMenu10", "sEnchantmentMenu11", "sEnchantmentMenu12", "sEnchantmentMenu2", "sEnchantmentMenu3", "sEnchantmentMenu4", "sEnchantmentMenu5", "sEnchantmentMenu6", "sEnchantmentMenu7", "sEnchantmentMenu8", "sEnchantmentMenu9", "sEncumbrance", "sEndDesc", "sEquip", "sExitGame", "sExpelled", "sExpelledMessage", "sFace", "sFaction", "sFar", "sFast", "sFatDesc", "sFatigue", "sFavoriteSkills", "sfeet", "sFileSize", "sfootarea", "sFootsteps", "sfor", "sFortify", "sForward", "sForwardXbox", "sFull", "sGame", "sGameWithoutLauncherXbox", "sGamma_Correction", "sGeneralMastPlugMismatchMsg", "sGold", "sGoodbye", "sGoverningAttribute", "sgp", "sHair", "sHard", "sHeal", "sHealer", "sHealth", "sHealthDesc", "sHealthPerHourOfRest", "sHealthPerLevel", "sHeavy", "sHigh", "sin", "sInfo", "sInfoRefusal", "sIngredients", "sInPrisonTitle", "sInputMenu1", "sIntDesc", "sIntimidate", "sIntimidateFail", "sIntimidateSuccess", "sInvalidSaveGameMsg", "sInvalidSaveGameMsgXBOX", "sInventory", "sInventoryMenu1", "sInventoryMessage1", "sInventoryMessage2", "sInventoryMessage3", "sInventoryMessage4", "sInventoryMessage5", "sInventorySelectNoIngredients", "sInventorySelectNoItems", "sInventorySelectNoSoul", "sItem", "sItemCastConstant", "sItemCastOnce", "sItemCastWhenStrikes", "sItemCastWhenUsed", "sItemName", "sJournal", "sJournalCmd", "sJournalEntry", "sJournalXbox", "sJoystickHatShort", "sJoystickNotFound", "sJoystickShort", "sJump", "sJumpXbox", "sKeyName_00", "sKeyName_01", "sKeyName_02", "sKeyName_03", "sKeyName_04", "sKeyName_05", "sKeyName_06", "sKeyName_07", "sKeyName_08", "sKeyName_09", "sKeyName_0A", "sKeyName_0B", "sKeyName_0C", "sKeyName_0D", "sKeyName_0E", "sKeyName_0F", "sKeyName_10", "sKeyName_11", "sKeyName_12", "sKeyName_13", "sKeyName_14", "sKeyName_15", "sKeyName_16", "sKeyName_17", "sKeyName_18", "sKeyName_19", "sKeyName_1A", "sKeyName_1B", "sKeyName_1C", "sKeyName_1D", "sKeyName_1E", "sKeyName_1F", "sKeyName_20", "sKeyName_21", "sKeyName_22", "sKeyName_23", "sKeyName_24", "sKeyName_25", "sKeyName_26", "sKeyName_27", "sKeyName_28", "sKeyName_29", "sKeyName_2A", "sKeyName_2B", "sKeyName_2C", "sKeyName_2D", "sKeyName_2E", "sKeyName_2F", "sKeyName_30", "sKeyName_31", "sKeyName_32", "sKeyName_33", "sKeyName_34", "sKeyName_35", "sKeyName_36", "sKeyName_37", "sKeyName_38", "sKeyName_39", "sKeyName_3A", "sKeyName_3B", "sKeyName_3C", "sKeyName_3D", "sKeyName_3E", "sKeyName_3F", "sKeyName_40", "sKeyName_41", "sKeyName_42", "sKeyName_43", "sKeyName_44", "sKeyName_45", "sKeyName_46", "sKeyName_47", "sKeyName_48", "sKeyName_49", "sKeyName_4A", "sKeyName_4B", "sKeyName_4C", "sKeyName_4D", "sKeyName_4E", "sKeyName_4F", "sKeyName_50", "sKeyName_51", "sKeyName_52", "sKeyName_53", "sKeyName_54", "sKeyName_55", "sKeyName_56", "sKeyName_57", "sKeyName_58", "sKeyName_59", "sKeyName_5A", "sKeyName_5B", "sKeyName_5C", "sKeyName_5D", "sKeyName_5E", "sKeyName_5F", "sKeyName_60", "sKeyName_61", "sKeyName_62", "sKeyName_63", "sKeyName_64", "sKeyName_65", "sKeyName_66", "sKeyName_67", "sKeyName_68", "sKeyName_69", "sKeyName_6A", "sKeyName_6B", "sKeyName_6C", "sKeyName_6D", "sKeyName_6E", "sKeyName_6F", "sKeyName_70", "sKeyName_71", "sKeyName_72", "sKeyName_73", "sKeyName_74", "sKeyName_75", "sKeyName_76", "sKeyName_77", "sKeyName_78", "sKeyName_79", "sKeyName_7A", "sKeyName_7B", "sKeyName_7C", "sKeyName_7D", "sKeyName_7E", "sKeyName_7F", "sKeyName_80", "sKeyName_81", "sKeyName_82", "sKeyName_83", "sKeyName_84", "sKeyName_85", "sKeyName_86", "sKeyName_87", "sKeyName_88", "sKeyName_89", "sKeyName_8A", "sKeyName_8B", "sKeyName_8C", "sKeyName_8D", "sKeyName_8E", "sKeyName_8F", "sKeyName_90", "sKeyName_91", "sKeyName_92", "sKeyName_93", "sKeyName_94", "sKeyName_95", "sKeyName_96", "sKeyName_97", "sKeyName_98", "sKeyName_99", "sKeyName_9A", "sKeyName_9B", "sKeyName_9C", "sKeyName_9D", "sKeyName_9E", "sKeyName_9F", "sKeyName_A0", "sKeyName_A1", "sKeyName_A2", "sKeyName_A3", "sKeyName_A4", "sKeyName_A5", "sKeyName_A6", "sKeyName_A7", "sKeyName_A8", "sKeyName_A9", "sKeyName_AA", "sKeyName_AB", "sKeyName_AC", "sKeyName_AD", "sKeyName_AE", "sKeyName_AF", "sKeyName_B0", "sKeyName_B1", "sKeyName_B2", "sKeyName_B3", "sKeyName_B4", "sKeyName_B5", "sKeyName_B6", "sKeyName_B7", "sKeyName_B8", "sKeyName_B9", "sKeyName_BA", "sKeyName_BB", "sKeyName_BC", "sKeyName_BD", "sKeyName_BE", "sKeyName_BF", "sKeyName_C0", "sKeyName_C1", "sKeyName_C2", "sKeyName_C3", "sKeyName_C4", "sKeyName_C5", "sKeyName_C6", "sKeyName_C7", "sKeyName_C8", "sKeyName_C9", "sKeyName_CA", "sKeyName_CB", "sKeyName_CC", "sKeyName_CD", "sKeyName_CE", "sKeyName_CF", "sKeyName_D0", "sKeyName_D1", "sKeyName_D2", "sKeyName_D3", "sKeyName_D4", "sKeyName_D5", "sKeyName_D6", "sKeyName_D7", "sKeyName_D8", "sKeyName_D9", "sKeyName_DA", "sKeyName_DB", "sKeyName_DC", "sKeyName_DD", "sKeyName_DE", "sKeyName_DF", "sKeyName_E0", "sKeyName_E1", "sKeyName_E2", "sKeyName_E3", "sKeyName_E4", "sKeyName_E5", "sKeyName_E6", "sKeyName_E7", "sKeyName_E8", "sKeyName_E9", "sKeyName_EA", "sKeyName_EB", "sKeyName_EC", "sKeyName_ED", "sKeyName_EE", "sKeyName_EF", "sKeyName_F0", "sKeyName_F1", "sKeyName_F2", "sKeyName_F3", "sKeyName_F4", "sKeyName_F5", "sKeyName_F6", "sKeyName_F7", "sKeyName_F8", "sKeyName_F9", "sKeyName_FA", "sKeyName_FB", "sKeyName_FC", "sKeyName_FD", "sKeyName_FE", "sKeyName_FF", "sKeyUsed", "sKilledEssential", "sKnight", "sLeft", "sLess", "sLevel", "sLevelProgress", "sLevels", "sLevelUp", "sLevelUpMenu1", "sLevelUpMenu2", "sLevelUpMenu3", "sLevelUpMenu4", "sLevelUpMsg", "sLevitateDisabled", "sLight", "sLight_Gamma", "sLoadFailedMessage", "sLoadGame", "sLoadingErrorsMsg", "sLoadingMessage1", "sLoadingMessage14", "sLoadingMessage15", "sLoadingMessage2", "sLoadingMessage3", "sLoadingMessage4", "sLoadingMessage5", "sLoadingMessage9", "sLoadLastSaveMsg", "sLocal", "sLockFail", "sLockImpossible", "sLockLevel", "sLockSuccess", "sLookDownXbox", "sLookUpXbox", "sLow", "sLucDesc", "sMagDesc", "sMage", "sMagic", "sMagicAncestralGhostID", "sMagicBonelordID", "sMagicBoundBattleAxeID", "sMagicBoundBootsID", "sMagicBoundCuirassID", "sMagicBoundDaggerID", "sMagicBoundHelmID", "sMagicBoundLeftGauntletID", "sMagicBoundLongbowID", "sMagicBoundLongswordID", "sMagicBoundMaceID", "sMagicBoundRightGauntletID", "sMagicBoundShieldID", "sMagicBoundSpearID", "sMagicCannotRecast", "sMagicCenturionSphereID", "sMagicClannfearID", "sMagicContractDisease", "sMagicCorprusWorsens", "sMagicCreature01ID", "sMagicCreature02ID", "sMagicCreature03ID", "sMagicCreature04ID", "sMagicCreature05ID", "sMagicDaedrothID", "sMagicDremoraID", "sMagicEffects", "sMagicFabricantID", "sMagicFlameAtronachID", "sMagicFrostAtronachID", "sMagicGoldenSaintID", "sMagicGreaterBonewalkerID", "sMagicHungerID", "sMagicInsufficientCharge", "sMagicInsufficientSP", "sMagicInvalidEffect", "sMagicInvalidTarget", "sMagicItem", "sMagicLeastBonewalkerID", "sMagicLockSuccess", "sMagicMenu", "sMagicOpenSuccess", "sMagicPCResisted", "sMagicScampID", "sMagicSelectTitle", "sMagicSkeletalMinionID", "sMagicSkillFail", "sMagicStormAtronachID", "sMagicTab", "sMagicTargetResisted", "sMagicTargetResistsWeapons", "sMagicWingedTwilightID", "sMagnitude", "sMagnitudeDes", "sMake Enchantment", "sMap", "sMaster", "sMastPlugMismatchMsg", "sMaximumSaveGameMessage", "sMaxSale", "sMedium", "sMenu_Help_Delay", "sMenu_Mode", "sMenuModeXbox", "sMenuNextXbox", "sMenuPrevXbox", "sMenus", "sMessage1", "sMessage2", "sMessage3", "sMessage4", "sMessage5", "sMessageQuestionAnswer1", "sMessageQuestionAnswer2", "sMessageQuestionAnswer3", "sMiscTab", "sMissingMastersMsg", "sMonk", "sMonthEveningstar", "sMonthFirstseed", "sMonthFrostfall", "sMonthHeartfire", "sMonthLastseed", "sMonthMidyear", "sMonthMorningstar", "sMonthRainshand", "sMonthSecondseed", "sMonthSunsdawn", "sMonthSunsdusk", "sMonthSunsheight", "sMore", "sMortar", "sMouse", "sMouseFlip", "sMouseWheelDownShort", "sMouseWheelUpShort", "sMove", "sMoveDownXbox", "sMoveUpXbox", "sMusic", "sName", "sNameTitle", "sNear", "sNeedOneSkill", "sNeedTwoSkills", "sNewGame", "sNext", "sNextRank", "sNextSpell", "sNextSpellXbox", "sNextWeapon", "sNextWeaponXbox", "sNightblade", "sNo", "sNoName", "sNone", "sNotifyMessage1", "sNotifyMessage10", "sNotifyMessage11", "sNotifyMessage12", "sNotifyMessage13", "sNotifyMessage14", "sNotifyMessage15", "sNotifyMessage16", "sNotifyMessage16_a", "sNotifyMessage17", "sNotifyMessage18", "sNotifyMessage19", "sNotifyMessage2", "sNotifyMessage20", "sNotifyMessage21", "sNotifyMessage22", "sNotifyMessage23", "sNotifyMessage24", "sNotifyMessage25", "sNotifyMessage26", "sNotifyMessage27", "sNotifyMessage28", "sNotifyMessage29", "sNotifyMessage3", "sNotifyMessage30", "sNotifyMessage31", "sNotifyMessage32", "sNotifyMessage33", "sNotifyMessage34", "sNotifyMessage35", "sNotifyMessage36", "sNotifyMessage37", "sNotifyMessage38", "sNotifyMessage39", "sNotifyMessage4", "sNotifyMessage40", "sNotifyMessage41", "sNotifyMessage42", "sNotifyMessage43", "sNotifyMessage44", "sNotifyMessage45", "sNotifyMessage46", "sNotifyMessage47", "sNotifyMessage48", "sNotifyMessage49", "sNotifyMessage4XBOX", "sNotifyMessage5", "sNotifyMessage50", "sNotifyMessage51", "sNotifyMessage52", "sNotifyMessage53", "sNotifyMessage54", "sNotifyMessage55", "sNotifyMessage56", "sNotifyMessage57", "sNotifyMessage58", "sNotifyMessage59", "sNotifyMessage6", "sNotifyMessage60", "sNotifyMessage61", "sNotifyMessage62", "sNotifyMessage63", "sNotifyMessage64", "sNotifyMessage65", "sNotifyMessage66", "sNotifyMessage67", "sNotifyMessage6a", "sNotifyMessage7", "sNotifyMessage8", "sNotifyMessage9", "sOff", "sOffer", "sOfferMenuTitle", "sOK", "sOn", "sOnce", "sOneHanded", "sOnetypeEffectMessage", "sonword", "sOptions", "sOptionsMenuXbox", "spercent", "sPerDesc", "sPersuasion", "sPersuasionMenuTitle", "sPickUp", "sPilgrim", "spoint", "spoints", "sPotionSuccess", "sPowerAlreadyUsed", "sPowers", "sPreferences", "sPrefs", "sPrev", "sPrevSpell", "sPrevSpellXbox", "sPrevWeapon", "sPrevWeaponXbox", "sProfitValue", "sQuality", "sQuanityMenuMessage01", "sQuanityMenuMessage02", "sQuestionDeleteSpell", "sQuestionMark", "sQuick0Xbox", "sQuick10Cmd", "sQuick1Cmd", "sQuick2Cmd", "sQuick3Cmd", "sQuick4Cmd", "sQuick4Xbox", "sQuick5Cmd", "sQuick5Xbox", "sQuick6Cmd", "sQuick6Xbox", "sQuick7Cmd", "sQuick7Xbox", "sQuick8Cmd", "sQuick8Xbox", "sQuick9Cmd", "sQuick9Xbox", "sQuick_Save", "sQuickLoadCmd", "sQuickLoadXbox", "sQuickMenu", "sQuickMenu1", "sQuickMenu2", "sQuickMenu3", "sQuickMenu4", "sQuickMenu5", "sQuickMenu6", "sQuickMenuInstruc", "sQuickMenuTitle", "sQuickSaveCmd", "sQuickSaveXbox", "sRace", "sRaceMenu1", "sRaceMenu2", "sRaceMenu3", "sRaceMenu4", "sRaceMenu5", "sRaceMenu6", "sRaceMenu7", "sRacialTraits", "sRange", "sRangeDes", "sRangeSelf", "sRangeTarget", "sRangeTouch", "sReady_Magic", "sReady_Weapon", "sReadyItemXbox", "sReadyMagicXbox", "sRechargeEnchantment", "sRender_Distance", "sRepair", "sRepairFailed", "sRepairServiceTitle", "sRepairSuccess", "sReputation", "sResChangeWarning", "sRest", "sRestIllegal", "sRestKey", "sRestMenu1", "sRestMenu2", "sRestMenu3", "sRestMenu4", "sRestMenuXbox", "sRestore", "sRetort", "sReturnToGame", "sRight", "sRogue", "sRun", "sRunXbox", "sSave", "sSaveGame", "sSaveGameDenied", "sSaveGameFailed", "sSaveGameNoMemory", "sSaveGameTooBig", "sSaveMenu1", "sSaveMenuHelp01", "sSaveMenuHelp02", "sSaveMenuHelp03", "sSaveMenuHelp04", "sSaveMenuHelp05", "sSaveMenuHelp06", "sSchool", "sSchoolAlteration", "sSchoolConjuration", "sSchoolDestruction", "sSchoolIllusion", "sSchoolMysticism", "sSchoolRestoration", "sScout", "sScrolldown", "sScrollup", "ssecond", "sseconds", "sSeldom", "sSelect", "sSell", "sSellerGold", "sService", "sServiceRefusal", "sServiceRepairTitle", "sServiceSpellsTitle", "sServiceTrainingTitle", "sServiceTrainingWords", "sServiceTravelTitle", "sSetValueMessage01", "sSex", "sShadows", "sShadowText", "sShift", "sSkill", "sSkillAcrobatics", "sSkillAlchemy", "sSkillAlteration", "sSkillArmorer", "sSkillAthletics", "sSkillAxe", "sSkillBlock", "sSkillBluntweapon", "sSkillClassMajor", "sSkillClassMinor", "sSkillClassMisc", "sSkillConjuration", "sSkillDestruction", "sSkillEnchant", "sSkillHandtohand", "sSkillHeavyarmor", "sSkillIllusion", "sSkillLightarmor", "sSkillLongblade", "sSkillMarksman", "sSkillMaxReached", "sSkillMediumarmor", "sSkillMercantile", "sSkillMysticism", "sSkillProgress", "sSkillRestoration", "sSkillSecurity", "sSkillShortblade", "sSkillsMenu1", "sSkillsMenuReputationHelp", "sSkillSneak", "sSkillSpear", "sSkillSpeechcraft", "sSkillUnarmored", "sSlash", "sSleepInterrupt", "sSlideLeftXbox", "sSlideRightXbox", "sSlow", "sSorceror", "sSoulGem", "sSoulGemsWithSouls", "sSoultrapSuccess", "sSpace", "sSpdDesc", "sSpecialization", "sSpecializationCombat", "sSpecializationMagic", "sSpecializationMenu1", "sSpecializationStealth", "sSpellmaking", "sSpellmakingHelp1", "sSpellmakingHelp2", "sSpellmakingHelp3", "sSpellmakingHelp4", "sSpellmakingHelp5", "sSpellmakingHelp6", "sSpellmakingMenu1", "sSpellmakingMenuTitle", "sSpells", "sSpellServiceTitle", "sSpellsword", "sStartCell", "sStartCellError", "sStartError", "sStats", "sStrafe", "sStrDesc", "sStrip", "sSubtitles", "sSystemMenuXbox", "sTake", "sTakeAll", "sTargetCriticalStrike", "sTaunt", "sTauntFail", "sTauntSuccess", "sTeleportDisabled", "sThief", "sThrust", "sTo", "sTogglePOVCmd", "sTogglePOVXbox", "sToggleRunXbox", "sTopics", "sTotalCost", "sTotalSold", "sTraining", "sTrainingServiceTitle", "sTraits", "sTransparency_Menu", "sTrapFail", "sTrapImpossible", "sTrapped", "sTrapSuccess", "sTravel", "sTravelServiceTitle", "sTurn", "sTurnLeftXbox", "sTurnRightXbox", "sTwoHanded", "sType", "sTypeAbility", "sTypeBlightDisease", "sTypeCurse", "sTypeDisease", "sTypePower", "sTypeSpell", "sUnequip", "sUnlocked", "sUntilHealed", "sUse", "sUserDefinedClass", "sUses", "sUseXbox", "sValue", "sVideo", "sVideoWarning", "sVoice", "sWait", "sWarrior", "sWaterReflectUpdate", "sWaterTerrainReflect", "sWeaponTab", "sWeight", "sWerewolfAlarmMessage", "sWerewolfPopup", "sWerewolfRefusal", "sWerewolfRestMessage", "sWilDesc", "sWitchhunter", "sWorld", "sWornTab", "sXStrafe", "sXTimes", "sXTimesINT", "sYes", "sYourGold", }; const char* CSMWorld::DefaultGmsts::OptionalFloats[CSMWorld::DefaultGmsts::OptionalFloatCount] = { "fCombatDistanceWerewolfMod", "fFleeDistance", "fWereWolfAcrobatics", "fWereWolfAgility", "fWereWolfAlchemy", "fWereWolfAlteration", "fWereWolfArmorer", "fWereWolfAthletics", "fWereWolfAxe", "fWereWolfBlock", "fWereWolfBluntWeapon", "fWereWolfConjuration", "fWereWolfDestruction", "fWereWolfEnchant", "fWereWolfEndurance", "fWereWolfFatigue", "fWereWolfHandtoHand", "fWereWolfHealth", "fWereWolfHeavyArmor", "fWereWolfIllusion", "fWereWolfIntellegence", "fWereWolfLightArmor", "fWereWolfLongBlade", "fWereWolfLuck", "fWereWolfMagicka", "fWereWolfMarksman", "fWereWolfMediumArmor", "fWereWolfMerchantile", "fWereWolfMysticism", "fWereWolfPersonality", "fWereWolfRestoration", "fWereWolfRunMult", "fWereWolfSecurity", "fWereWolfShortBlade", "fWereWolfSilverWeaponDamageMult", "fWereWolfSneak", "fWereWolfSpear", "fWereWolfSpeechcraft", "fWereWolfSpeed", "fWereWolfStrength", "fWereWolfUnarmored", "fWereWolfWillPower", }; const char* CSMWorld::DefaultGmsts::OptionalInts[CSMWorld::DefaultGmsts::OptionalIntCount] = { "iWereWolfBounty", "iWereWolfFightMod", "iWereWolfFleeMod", "iWereWolfLevelToAttack", }; const char* CSMWorld::DefaultGmsts::OptionalStrings[CSMWorld::DefaultGmsts::OptionalStringCount] = { "sCompanionShare", "sCompanionWarningButtonOne", "sCompanionWarningButtonTwo", "sCompanionWarningMessage", "sDeleteNote", "sEditNote", "sEffectSummonCreature01", "sEffectSummonCreature02", "sEffectSummonCreature03", "sEffectSummonCreature04", "sEffectSummonCreature05", "sEffectSummonFabricant", "sLevitateDisabled", "sMagicCreature01ID", "sMagicCreature02ID", "sMagicCreature03ID", "sMagicCreature04ID", "sMagicCreature05ID", "sMagicFabricantID", "sMaxSale", "sProfitValue", "sTeleportDisabled", "sWerewolfAlarmMessage", "sWerewolfPopup", "sWerewolfRefusal", "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 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 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 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 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 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 1000.0f, // fTargetSpellMaxSpeed 1000.0f, // fThrownWeaponMaxSpeed 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 }; 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 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 }; openmw-openmw-0.49.0/apps/opencs/model/world/defaultgmsts.hpp000066400000000000000000000015041503074453300243140ustar00rootroot00000000000000#ifndef CSM_WORLD_DEFAULTGMSTS_H #define CSM_WORLD_DEFAULTGMSTS_H #include 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 float FloatsDefaultValues[]; extern const int IntsDefaultValues[]; extern const float FloatLimits[]; extern const int IntLimits[]; } } #endif openmw-openmw-0.49.0/apps/opencs/model/world/disabletag.hpp000066400000000000000000000007031503074453300237110ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/model/world/idcollection.cpp000066400000000000000000000126551503074453300242660ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/opencs/model/world/idcollection.hpp000066400000000000000000000140171503074453300242650ustar00rootroot00000000000000#ifndef CSM_WOLRD_IDCOLLECTION_H #define CSM_WOLRD_IDCOLLECTION_H #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 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 { }; 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: const Record* searchRecord(std::uint16_t index, int plugin) const; const std::string* getLandTexture(std::uint16_t index, int plugin) const; bool touchRecord(const ESM::RefId& id) override; 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 BaseIdCollection::loadRecord( ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { record.load(reader, isDeleted); } template <> inline void BaseIdCollection::loadRecord(Land& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { 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); // Prevent data from being reloaded. record.mContext.filename.clear(); if (!base) record.setPlugin(-1); } template int BaseIdCollection::load(ESM::ESMReader& reader, bool base) { ESXRecordT record; bool isDeleted = false; loadRecord(record, reader, isDeleted, base); ESM::RefId id = getRecordId(record); int index = this->searchId(id); 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); } template int BaseIdCollection::load(const ESXRecordT& record, bool base, int index) { if (index == -2) // index unknown index = this->searchId(getRecordId(record)); if (index == -1) { // new 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(std::move(record2)); } else { // old record auto record2 = std::make_unique>(Collection::getRecord(index)); if (base) record2->mBase = record; else record2->setModified(record); this->setRecord(index, std::move(record2)); } return index; } template bool BaseIdCollection::tryDelete(const ESM::RefId& id) { int index = this->searchId(id); if (index == -1) return false; const Record& record = Collection::getRecord(index); if (record.isDeleted()) return false; if (record.mState == RecordBase::State_ModifiedOnly) { Collection::removeRows(index, 1); } else { 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 openmw-openmw-0.49.0/apps/opencs/model/world/idcompletionmanager.cpp000066400000000000000000000127041503074453300256320ustar00rootroot00000000000000#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_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; return types; } typedef std::map::const_iterator ModelTypeConstIterator; } const std::map CSMWorld::IdCompletionManager::sCompleterModelTypes = generateModelTypes(); std::vector CSMWorld::IdCompletionManager::getDisplayTypes() { std::vector types; ModelTypeConstIterator current = sCompleterModelTypes.begin(); ModelTypeConstIterator end = sCompleterModelTypes.end(); for (; current != end; ++current) { types.push_back(current->first); } // Hack for Display_InfoCondVar types.push_back(CSMWorld::ColumnBase::Display_InfoCondVar); return types; } CSMWorld::IdCompletionManager::IdCompletionManager(CSMWorld::Data& data) { generateCompleters(data); } bool CSMWorld::IdCompletionManager::hasCompleterFor(CSMWorld::ColumnBase::Display display) const { return mCompleters.find(display) != mCompleters.end(); } std::shared_ptr CSMWorld::IdCompletionManager::getCompleter(CSMWorld::ColumnBase::Display display) { if (!hasCompleterFor(display)) { throw std::logic_error("This column doesn't have an ID completer"); } return mCompleters[display]; } 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); if (table != nullptr) { int idColumn = table->searchColumnIndex(CSMWorld::Columns::ColumnId_Id); if (idColumn != -1) { std::shared_ptr completer = std::make_shared(table); completer->setCompletionColumn(idColumn); // The completion role must be Qt::DisplayRole to get the ID values from the model completer->setCompletionRole(Qt::DisplayRole); completer->setCaseSensitivity(Qt::CaseInsensitive); QAbstractItemView* popup = new CSVWidget::CompleterPopup(); completer->setPopup(popup); // The completer takes ownership of the popup completer->setMaxVisibleItems(10); mCompleters[current->first] = std::move(completer); } } } } openmw-openmw-0.49.0/apps/opencs/model/world/idcompletionmanager.hpp000066400000000000000000000017441503074453300256410ustar00rootroot00000000000000#ifndef CSM_WORLD_IDCOMPLETIONMANAGER_HPP #define CSM_WORLD_IDCOMPLETIONMANAGER_HPP #include #include #include #include "columnbase.hpp" #include "universalid.hpp" class QCompleter; namespace CSMWorld { class Data; /// \brief Creates and stores all ID completers class IdCompletionManager { static const std::map sCompleterModelTypes; std::map> mCompleters; // Don't allow copying IdCompletionManager(const IdCompletionManager&); IdCompletionManager& operator=(const IdCompletionManager&); void generateCompleters(Data& data); public: static std::vector getDisplayTypes(); IdCompletionManager(Data& data); bool hasCompleterFor(ColumnBase::Display display) const; std::shared_ptr getCompleter(ColumnBase::Display display); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/idtable.cpp000066400000000000000000000251101503074453300232100ustar00rootroot00000000000000#include "idtable.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "collectionbase.hpp" #include "columnbase.hpp" CSMWorld::IdTable::IdTable(CollectionBase* idCollection, unsigned int features) : IdTableBase(features) , mIdCollection(idCollection) { } int CSMWorld::IdTable::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return mIdCollection->getSize(); } int CSMWorld::IdTable::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return mIdCollection->getColumns(); } QVariant CSMWorld::IdTable::data(const QModelIndex& index, int role) const { if (index.row() < 0 || index.column() < 0) return QVariant(); if (role == ColumnBase::Role_Display) return QVariant(mIdCollection->getColumn(index.column()).mDisplayType); if (role == ColumnBase::Role_ColumnId) return QVariant(getColumnId(index.column())); if ((role != Qt::DisplayRole && role != Qt::EditRole)) return QVariant(); if (role == Qt::EditRole && !mIdCollection->getColumn(index.column()).isEditable()) return QVariant(); return mIdCollection->getData(index.row(), index.column()); } QVariant CSMWorld::IdTable::headerData(int section, Qt::Orientation orientation, int role) const { 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 == ColumnBase::Role_Flags) return mIdCollection->getColumn(section).mFlags; if (role == ColumnBase::Role_Display) return mIdCollection->getColumn(section).mDisplayType; if (role == ColumnBase::Role_ColumnId) return getColumnId(section); return QVariant(); } bool CSMWorld::IdTable::setData(const QModelIndex& index, const QVariant& value, int role) { if (mIdCollection->getColumn(index.column()).isEditable() && role == Qt::EditRole) { mIdCollection->setData(index.row(), index.column(), value); int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) { if (index.column() == stateColumn) { // 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(index, index); // Modifying a value can also change the Modified status of a record. QModelIndex stateIndex = this->index(index.row(), stateColumn); emit dataChanged(stateIndex, stateIndex); } } else emit dataChanged(index, index); return true; } return false; } 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()) 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) { if (parent.isValid()) return false; beginRemoveRows(parent, row, row + count - 1); mIdCollection->removeRows(row, count); endRemoveRows(); return true; } QModelIndex CSMWorld::IdTable::index(int row, int column, const QModelIndex& parent) const { if (parent.isValid()) return QModelIndex(); if (row < 0 || row >= mIdCollection->getSize()) return QModelIndex(); if (column < 0 || column >= mIdCollection->getColumns()) return QModelIndex(); return createIndex(row, column); } QModelIndex CSMWorld::IdTable::parent(const QModelIndex& index) const { return QModelIndex(); } void CSMWorld::IdTable::addRecord(const std::string& id, UniversalId::Type type) { ESM::RefId refId = ESM::RefId::stringRefId(id); int index = mIdCollection->getAppendIndex(refId, type); beginInsertRows(QModelIndex(), index, index); mIdCollection->appendBlankRecord(refId, type); endInsertRows(); } void CSMWorld::IdTable::addRecordWithData( const std::string& id, const std::map& data, UniversalId::Type type) { ESM::RefId refId = ESM::RefId::stringRefId(id); int index = mIdCollection->getAppendIndex(refId, type); beginInsertRows(QModelIndex(), index, index); mIdCollection->appendBlankRecord(refId, type); for (std::map::const_iterator iter(data.begin()); iter != data.end(); ++iter) { mIdCollection->setData(index, iter->first, iter->second); } endInsertRows(); } void CSMWorld::IdTable::cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, CSMWorld::UniversalId::Type type) { int index = mIdCollection->getAppendIndex(destination, type); beginInsertRows(QModelIndex(), index, index); mIdCollection->cloneRecord(origin, destination, type); endInsertRows(); } bool CSMWorld::IdTable::touchRecord(const std::string& id) { ESM::RefId refId = ESM::RefId::stringRefId(id); bool changed = mIdCollection->touchRecord(refId); int row = mIdCollection->getIndex(refId); int column = mIdCollection->searchColumnIndex(Columns::ColumnId_RecordType); if (changed && column != -1) { QModelIndex modelIndex = index(row, column); emit dataChanged(modelIndex, modelIndex); } return changed; } std::string CSMWorld::IdTable::getId(int row) const { 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 { 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, std::unique_ptr record, CSMWorld::UniversalId::Type type) { const ESM::RefId refId = ESM::RefId::stringRefId(id); const int index = mIdCollection->searchId(refId); if (index == -1) { // 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(), index2, index2); mIdCollection->appendRecord(std::move(record), type); endInsertRows(); } else { 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 { return mIdCollection->getRecord(ESM::RefId::stringRefId(id)); } int CSMWorld::IdTable::searchColumnIndex(Columns::ColumnId id) const { return mIdCollection->searchColumnIndex(id); } int CSMWorld::IdTable::findColumnIndex(Columns::ColumnId id) const { return mIdCollection->findColumnIndex(id); } void CSMWorld::IdTable::reorderRows(int baseIndex, const std::vector& newOrder) { 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::string id; std::string hint; if (getFeatures() & Feature_ViewCell) { int cellColumn = mIdCollection->searchColumnIndex(Columns::ColumnId_Cell); int idColumn = mIdCollection->searchColumnIndex(Columns::ColumnId_Id); if (cellColumn != -1 && idColumn != -1) { 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); if (column != -1) { id = mIdCollection->getData(row, column).toString().toUtf8().constData(); hint = "c:" + id; } } if (id.empty()) return std::make_pair(UniversalId::Type_None, ""); if (id[0] == '#') id = ESM::Cell::sDefaultWorldspaceId.getValue(); return std::make_pair(UniversalId(UniversalId::Type_Scene, id), hint); } /// For top level data/columns bool CSMWorld::IdTable::isDeleted(const std::string& id) const { return getRecord(id).isDeleted(); } int CSMWorld::IdTable::getColumnId(int column) const { return mIdCollection->getColumn(column).getId(); } CSMWorld::CollectionBase* CSMWorld::IdTable::idCollection() const { return mIdCollection; } CSMWorld::LandTextureIdTable::LandTextureIdTable(CollectionBase* idCollection, unsigned int features) : IdTable(idCollection, features) { } const CSMWorld::Record* CSMWorld::LandTextureIdTable::searchRecord( std::uint16_t index, int plugin) const { return static_cast*>(idCollection())->searchRecord(index, plugin); } openmw-openmw-0.49.0/apps/opencs/model/world/idtable.hpp000066400000000000000000000101051503074453300232130ustar00rootroot00000000000000#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" class QObject; namespace ESM { struct LandTexture; } namespace CSMWorld { class CollectionBase; struct RecordBase; template struct Record; class IdTable : public IdTableBase { Q_OBJECT private: CollectionBase* mIdCollection; // 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. virtual ~IdTable() = default; int rowCount(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 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; Qt::ItemFlags flags(const QModelIndex& index) const 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 parent(const QModelIndex& index) 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 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 cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, UniversalId::Type type = UniversalId::Type_None); bool touchRecord(const std::string& id); ///< Will change the record state to modified, if it is not already. std::string getId(int row) const; QModelIndex getModelIndex(const std::string& id, int column) const override; void setRecord( const std::string& id, std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None); ///< Add record or overwrite existing record. const RecordBase& getRecord(const std::string& id) const; 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 findColumnIndex(Columns::ColumnId id) const override; ///< Return index of column with the given \a id. If no such column exists, an exception is /// thrown. 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). 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, ""). /// 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; }; class LandTextureIdTable : public IdTable { public: LandTextureIdTable(CollectionBase* idCollection, unsigned int features = 0); const CSMWorld::Record* searchRecord(std::uint16_t index, int plugin) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/idtablebase.cpp000066400000000000000000000003071503074453300240440ustar00rootroot00000000000000#include "idtablebase.hpp" CSMWorld::IdTableBase::IdTableBase(unsigned int features) : mFeatures(features) { } unsigned int CSMWorld::IdTableBase::getFeatures() const { return mFeatures; } openmw-openmw-0.49.0/apps/opencs/model/world/idtablebase.hpp000066400000000000000000000037771503074453300240670ustar00rootroot00000000000000#ifndef CSM_WOLRD_IDTABLEBASE_H #define CSM_WOLRD_IDTABLEBASE_H #include #include #include #include #include "columns.hpp" namespace CSMWorld { class UniversalId; 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, /// 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, Feature_View = Feature_ViewId | Feature_ViewCell, Feature_Preview = 8, /// Table can not be modified through ordinary means. Feature_Constant = 16, Feature_AllowTouch = 32 }; private: unsigned int mFeatures; public: IdTableBase(unsigned int features); 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, 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; /// Is \a id flagged as deleted? virtual bool isDeleted(const std::string& id) const = 0; virtual int getColumnId(int column) const = 0; unsigned int getFeatures() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/idtableproxymodel.cpp000066400000000000000000000125521503074453300253410ustar00rootroot00000000000000#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) { if (index < 0 || index >= static_cast(values.size())) { return ""; } return values[index].second; } } void CSMWorld::IdTableProxyModel::updateColumnMap() { Q_ASSERT(mSourceModel != nullptr); mColumnMap.clear(); 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)))); } } bool CSMWorld::IdTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { Q_ASSERT(mSourceModel != nullptr); // It is not possible to use filterAcceptsColumn() and check for // sourceModel()->headerData (sourceColumn, Qt::Horizontal, CSMWorld::ColumnBase::Role_Flags) // because the sourceColumn parameter excludes the hidden columns, i.e. wrong columns can // be rejected. Workaround by disallowing tree branches (nested columns), which are not meant // to be visible, from the filter. if (sourceParent.isValid()) return false; if (!mFilter) return true; return mFilter->test(*mSourceModel, sourceRow, mColumnMap); } CSMWorld::IdTableProxyModel::IdTableProxyModel(QObject* parent) : QSortFilterProxyModel(parent) , mFilterTimer{ new QTimer(this) } , mSourceModel(nullptr) { 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 { Q_ASSERT(mSourceModel != nullptr); return mapFromSource(mSourceModel->getModelIndex(id, column)); } void CSMWorld::IdTableProxyModel::setSourceModel(QAbstractItemModel* model) { QSortFilterProxyModel::setSourceModel(model); 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) { mAwaitingFilter = filter; mFilterTimer->start(); } 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); if (valuesIt == mEnumColumnCache.end()) { if (Columns::hasEnums(id)) { valuesIt = mEnumColumnCache.insert(std::make_pair(id, Columns::getEnums(id))).first; } } if (valuesIt != mEnumColumnCache.end()) { std::string first = getEnumValue(valuesIt->second, left.data().toInt()); std::string second = getEnumValue(valuesIt->second, right.data().toInt()); return first < second; } return QSortFilterProxyModel::lessThan(left, right); } QString CSMWorld::IdTableProxyModel::getRecordId(int sourceRow) const { Q_ASSERT(mSourceModel != nullptr); int idColumn = mSourceModel->findColumnIndex(Columns::ColumnId_Id); return mSourceModel->data(mSourceModel->index(sourceRow, idColumn)).toString(); } void CSMWorld::IdTableProxyModel::refreshFilter() { 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) { refreshFilter(); if (!parent.isValid()) { emit rowAdded(getRecordId(end).toUtf8().constData()); } } void CSMWorld::IdTableProxyModel::sourceRowsRemoved(const QModelIndex& /*parent*/, int /*start*/, int /*end*/) { refreshFilter(); } void CSMWorld::IdTableProxyModel::sourceDataChanged(const QModelIndex& /*topLeft*/, const QModelIndex& /*bottomRight*/) { refreshFilter(); } openmw-openmw-0.49.0/apps/opencs/model/world/idtableproxymodel.hpp000066400000000000000000000042161503074453300253440ustar00rootroot00000000000000#ifndef CSM_WOLRD_IDTABLEPROXYMODEL_H #define CSM_WOLRD_IDTABLEPROXYMODEL_H #include #include #include #include #include #include #include #include #include #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::unique_ptr mFilterTimer; std::shared_ptr mAwaitingFilter; 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; protected: IdTableBase* mSourceModel; private: void updateColumnMap(); public: IdTableProxyModel(QObject* parent = nullptr); virtual QModelIndex getModelIndex(const std::string& id, int column) const; void setSourceModel(QAbstractItemModel* model) override; void setFilter(const std::shared_ptr& filter); void refreshFilter(); protected: bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; QString getRecordId(int sourceRow) const; protected slots: virtual void sourceRowsInserted(const QModelIndex& parent, int start, int end); virtual void sourceRowsRemoved(const QModelIndex& parent, int start, int end); virtual void sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void timerTimeout(); void settingChanged(const CSMPrefs::Setting* setting); signals: void rowAdded(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/idtree.cpp000066400000000000000000000215401503074453300230630ustar00rootroot00000000000000#include "idtree.hpp" #include "nestedtablewrapper.hpp" #include "collectionbase.hpp" #include "columnbase.hpp" #include "nestedcollection.hpp" #include #include #include // 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()); return IdTable::rowCount(parent); } int CSMWorld::IdTree::columnCount(const QModelIndex& parent) const { if (hasChildren(parent)) return mNestedCollection->getNestedColumnsCount(parent.row(), parent.column()); return IdTable::columnCount(parent); } QVariant CSMWorld::IdTree::data(const QModelIndex& index, int role) const { if (!index.isValid()) return QVariant(); if (index.internalId() != 0) { std::pair parentAddress(unfoldIndexAddress(index.internalId())); const NestableColumn* parentColumn = mNestedCollection->getNestableColumn(parentAddress.second); if (role == ColumnBase::Role_Display) return parentColumn->nestedColumn(index.column()).mDisplayType; if (role == ColumnBase::Role_ColumnId) return parentColumn->nestedColumn(index.column()).mColumnId; if (role == Qt::EditRole && !parentColumn->nestedColumn(index.column()).isEditable()) return QVariant(); if (role != Qt::DisplayRole && role != Qt::EditRole) return QVariant(); return mNestedCollection->getNestedData(parentAddress.first, parentAddress.second, index.row(), index.column()); } else { return IdTable::data(index, role); } } QVariant CSMWorld::IdTree::nestedHeaderData(int section, int subSection, Qt::Orientation orientation, int role) const { if (section < 0 || section >= idCollection()->getColumns()) return QVariant(); const NestableColumn* parentColumn = mNestedCollection->getNestableColumn(section); if (orientation == Qt::Vertical) return QVariant(); if (role == Qt::DisplayRole) return tr(parentColumn->nestedColumn(subSection).getTitle().c_str()); if (role == ColumnBase::Role_Flags) return parentColumn->nestedColumn(subSection).mFlags; if (role == ColumnBase::Role_Display) return parentColumn->nestedColumn(subSection).mDisplayType; if (role == ColumnBase::Role_ColumnId) return parentColumn->nestedColumn(subSection).mColumnId; return QVariant(); } 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) { const std::pair& parentAddress(unfoldIndexAddress(index.internalId())); 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. int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) { QModelIndex stateIndex = this->index(index.parent().row(), stateColumn); emit dataChanged(stateIndex, stateIndex); } return true; } else return false; } return IdTable::setData(index, value, role); } Qt::ItemFlags CSMWorld::IdTree::flags(const QModelIndex& index) const { if (!index.isValid()) return Qt::ItemFlags(); if (index.internalId() != 0) { std::pair parentAddress(unfoldIndexAddress(index.internalId())); Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; if (mNestedCollection->getNestableColumn(parentAddress.second)->nestedColumn(index.column()).isEditable()) flags |= Qt::ItemIsEditable; return flags; } else return IdTable::flags(index); } bool CSMWorld::IdTree::removeRows(int row, int count, const QModelIndex& parent) { if (parent.isValid()) { beginRemoveRows(parent, row, row + count - 1); for (int i = 0; i < count; ++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)); return true; } else return IdTable::removeRows(row, count, parent); } void CSMWorld::IdTree::addNestedRow(const QModelIndex& parent, int position) { if (!hasChildren(parent)) throw std::logic_error("Tried to set nested table, but index has no children"); int row = parent.row(); beginInsertRows(parent, position, position); mNestedCollection->addNestedRow(row, parent.column(), position); endInsertRows(); 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 { unsigned int encodedId = 0; if (parent.isValid()) { encodedId = this->foldIndexAddress(parent); } if (row < 0 || row >= rowCount(parent)) return QModelIndex(); if (column < 0 || column >= columnCount(parent)) return QModelIndex(); return createIndex(row, column, encodedId); // store internal id } QModelIndex CSMWorld::IdTree::getNestedModelIndex(const std::string& id, int column) const { return CSMWorld::IdTable::index(idCollection()->getIndex(ESM::RefId::stringRefId(id)), column); } 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(); unsigned int id = index.internalId(); const std::pair& address(unfoldIndexAddress(id)); if (address.first >= this->rowCount() || address.second >= this->columnCount()) throw std::logic_error("Parent index is not present in the model"); return createIndex(address.first, address.second); } unsigned int CSMWorld::IdTree::foldIndexAddress(const QModelIndex& index) const { unsigned int out = index.row() * this->columnCount(); out += index.column(); return ++out; } 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"); --id; int row = id / this->columnCount(); int column = id - row * this->columnCount(); return std::make_pair(row, column); } // FIXME: Not sure why this check is also needed? // // index.data().isValid() requires RefIdAdapter::getData() to return a valid QVariant for // nested columns (refidadapterimp.hpp) // // 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()); } void CSMWorld::IdTree::setNestedTable(const QModelIndex& index, const CSMWorld::NestedTableWrapperBase& nestedTable) { if (!hasChildren(index)) throw std::logic_error("Tried to set nested table, but index has no children"); bool removeRowsMode = false; if (nestedTable.size() != this->nestedTable(index)->size()) { emit resetStart(this->index(index.row(), 0).data().toString()); removeRowsMode = true; } mNestedCollection->setNestedTable(index.row(), index.column(), nestedTable); emit dataChanged(CSMWorld::IdTree::index(index.row(), 0), CSMWorld::IdTree::index(index.row(), idCollection()->getColumns() - 1)); if (removeRowsMode) { emit resetEnd(this->index(index.row(), 0).data().toString()); } } CSMWorld::NestedTableWrapperBase* CSMWorld::IdTree::nestedTable(const QModelIndex& index) const { if (!hasChildren(index)) throw std::logic_error("Tried to retrieve nested table, but index has no children"); return mNestedCollection->nestedTable(index.row(), index.column()); } int CSMWorld::IdTree::searchNestedColumnIndex(int parentColumn, Columns::ColumnId id) { return mNestedCollection->searchNestedColumnIndex(parentColumn, id); } int CSMWorld::IdTree::findNestedColumnIndex(int parentColumn, Columns::ColumnId id) { return mNestedCollection->findNestedColumnIndex(parentColumn, id); } openmw-openmw-0.49.0/apps/opencs/model/world/idtree.hpp000066400000000000000000000065011503074453300230700ustar00rootroot00000000000000#ifndef CSM_WOLRD_IDTREE_H #define CSM_WOLRD_IDTREE_H #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 * to the individiual fields of the records, Some records are holding nested data (for instance * inventory list of the npc). In cases like this, table model offers interface to access * nested data in the qt way - that is specify parent. Since some of those nested data require * multiple columns to represent information, single int (default way to index model in the * qmodelindex) is not sufficiant. Therefore tablemodelindex class can hold two ints for the * sake of indexing two dimensions of the table. This model does not support multiple levels of * the nested data. Vast majority of methods makes sense only for the top level data. */ namespace CSMWorld { class CollectionBase; class NestedCollection; struct NestedTableWrapperBase; class IdTree : public IdTable { Q_OBJECT private: NestedCollection* mNestedCollection; unsigned int foldIndexAddress(const QModelIndex& index) const; std::pair unfoldIndexAddress(unsigned int id) const; 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 columnCount(const QModelIndex& parent = QModelIndex()) 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; Qt::ItemFlags flags(const QModelIndex& index) const 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 parent(const QModelIndex& index) const override; QModelIndex getNestedModelIndex(const std::string& id, int column) const; QVariant nestedHeaderData( int section, int subSection, Qt::Orientation orientation, int role = Qt::DisplayRole) const; NestedTableWrapperBase* nestedTable(const QModelIndex& index) const; void setNestedTable(const QModelIndex& index, const NestedTableWrapperBase& nestedTable); void addNestedRow(const QModelIndex& parent, int position); 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 findNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or throws an exception if the requested column wasn't found. signals: void resetStart(const QString& id); void resetEnd(const QString& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/info.hpp000066400000000000000000000003541503074453300225470ustar00rootroot00000000000000#ifndef CSM_WOLRD_INFO_H #define CSM_WOLRD_INFO_H #include namespace CSMWorld { struct Info : public ESM::DialInfo { ESM::RefId mTopicId; ESM::RefId mOriginalId; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/infocollection.cpp000066400000000000000000000076251503074453300246260ustar00rootroot00000000000000#include "infocollection.hpp" #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 "collection.hpp" #include "info.hpp" namespace CSMWorld { namespace { std::string_view getInfoTopicId(const ESM::RefId& infoId) { return parseInfoRefId(infoId).first; } } ESM::RefId makeCompositeInfoRefId(const ESM::RefId& topicId, const ESM::RefId& infoId) { return ESM::RefId::stringRefId(topicId.getRefIdString() + '#' + infoId.getRefIdString()); } } void CSMWorld::InfoCollection::load(const Info& value, bool base) { const int index = searchId(value.mId); 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(std::move(record), getSize()); } else { // old record auto record = std::make_unique>(getRecord(index)); if (base) record->mBase = value; else record->setModified(value); setRecord(index, std::move(record)); } } void CSMWorld::InfoCollection::load( ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrders) { Info info; bool isDeleted = false; info.load(reader, isDeleted); const ESM::RefId id = makeCompositeInfoRefId(dialogue.mId, info.mId); if (isDeleted) { const int index = searchId(id); if (index == -1) { Log(Debug::Warning) << "Trying to delete absent info \"" << info.mId << "\" from topic \"" << dialogue.mId << "\""; return; } if (base) { infoOrders.at(dialogue.mId).removeInfo(info.mId); removeRows(index, 1); return; } auto record = std::make_unique>(getRecord(index)); record->mState = RecordBase::State_Deleted; setRecord(index, std::move(record)); return; } info.mTopicId = dialogue.mId; info.mOriginalId = info.mId; info.mId = id; load(info, base); infoOrders[dialogue.mId].insertInfo(OrderedInfo(info), isDeleted); } 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); } 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; } 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()); } bool CSMWorld::InfoCollection::reorderRows(int baseIndex, const std::vector& newOrder) { const int lastIndex = baseIndex + static_cast(newOrder.size()) - 1; if (lastIndex >= getSize()) return false; if (getRecord(baseIndex).get().mTopicId != getRecord(lastIndex).get().mTopicId) return false; return reorderRowsImp(baseIndex, newOrder); } openmw-openmw-0.49.0/apps/opencs/model/world/infocollection.hpp000066400000000000000000000027401503074453300246240ustar00rootroot00000000000000#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 { using InfosRecordPtrByTopic = std::unordered_map*>>; 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) { } }; using InfoOrder = ESM::InfoOrder; using InfoOrderByTopic = std::map>; class InfoCollection : public Collection { private: void load(const Info& value, bool base); public: void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrder); void sort(const InfoOrderByTopic& infoOrders); InfosRecordPtrByTopic getInfosByTopic() const; int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const override; bool reorderRows(int baseIndex, const std::vector& newOrder) override; }; ESM::RefId makeCompositeInfoRefId(const ESM::RefId& topicId, const ESM::RefId& infoId); } #endif openmw-openmw-0.49.0/apps/opencs/model/world/infoselectwrapper.cpp000066400000000000000000000611161503074453300253460ustar00rootroot00000000000000#include "infoselectwrapper.hpp" #include #include #include #include const char* CSMWorld::ConstInfoSelectWrapper::FunctionEnumStrings[] = { "Faction Reaction Low", "Faction Reaction High", "Rank Requirement", "Reputation", "Health Percent", "PC Reputation", "PC Level", "PC Health Percent", "PC Magicka", "PC Fatigue", "PC Strength", "PC Block", "PC Armorer", "PC Medium Armor", "PC Heavy Armor", "PC Blunt Weapon", "PC Long Blade", "PC Axe", "PC Spear", "PC Athletics", "PC Enchant", "PC Detruction", "PC Alteration", "PC Illusion", "PC Conjuration", "PC Mysticism", "PC Restoration", "PC Alchemy", "PC Unarmored", "PC Security", "PC Sneak", "PC Acrobatics", "PC Light Armor", "PC Short Blade", "PC Marksman", "PC Merchantile", "PC Speechcraft", "PC Hand to Hand", "PC Sex", "PC Expelled", "PC Common Disease", "PC Blight Disease", "PC Clothing Modifier", "PC Crime Level", "Same Sex", "Same Race", "Same Faction", "Faction Rank Difference", "Detected", "Alarmed", "Choice", "PC Intelligence", "PC Willpower", "PC Agility", "PC Speed", "PC Endurance", "PC Personality", "PC Luck", "PC Corprus", "Weather", "PC Vampire", "Level", "Attacked", "Talked to PC", "PC Health", "Creature Target", "Friend Hit", "Fight", "Hello", "Alarm", "Flee", "Should Attack", "Werewolf", "PC Werewolf Kills", "Global", "Local", "Journal", "Item", "Dead", "Not Id", "Not Faction", "Not Class", "Not Race", "Not Cell", "Not Local", nullptr, }; const char* CSMWorld::ConstInfoSelectWrapper::RelationEnumStrings[] = { "=", "!=", ">", ">=", "<", "<=", nullptr, }; namespace { 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_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)"; } } // ConstInfoSelectWrapper CSMWorld::ConstInfoSelectWrapper::ConstInfoSelectWrapper(const ESM::DialogueCondition& select) : mConstSelect(select) { updateHasVariable(); updateComparisonType(); } ESM::DialogueCondition::Function CSMWorld::ConstInfoSelectWrapper::getFunctionName() const { return mConstSelect.mFunction; } ESM::DialogueCondition::Comparison CSMWorld::ConstInfoSelectWrapper::getRelationType() const { return mConstSelect.mComparison; } CSMWorld::ConstInfoSelectWrapper::ComparisonType CSMWorld::ConstInfoSelectWrapper::getComparisonType() const { return mComparisonType; } bool CSMWorld::ConstInfoSelectWrapper::hasVariable() const { return mHasVariable; } const std::string& CSMWorld::ConstInfoSelectWrapper::getVariableName() const { return mConstSelect.mVariable; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue() const { if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { if (std::holds_alternative(mConstSelect.mValue)) return conditionIsAlwaysTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsAlwaysTrue(getConditionIntRange(), getValidFloatRange()); } return false; } bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue() const { if (mComparisonType == Comparison_Boolean || mComparisonType == Comparison_Integer) { if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidIntRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidIntRange()); } else if (mComparisonType == Comparison_Numeric) { if (std::holds_alternative(mConstSelect.mValue)) return conditionIsNeverTrue(getConditionFloatRange(), getValidFloatRange()); else return conditionIsNeverTrue(getConditionIntRange(), getValidFloatRange()); } return false; } std::string CSMWorld::ConstInfoSelectWrapper::toString() const { std::ostringstream stream; stream << convertToString(getFunctionName()) << " "; if (mHasVariable) stream << getVariableName() << " "; stream << convertToString(getRelationType()) << " "; std::visit([&](auto value) { stream << value; }, mConstSelect.mValue); return stream.str(); } void CSMWorld::ConstInfoSelectWrapper::updateHasVariable() { switch (getFunctionName()) { 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; default: mHasVariable = false; break; } } void CSMWorld::ConstInfoSelectWrapper::updateComparisonType() { switch (getFunctionName()) { // Boolean 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 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 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; default: mComparisonType = Comparison_None; break; } } std::pair CSMWorld::ConstInfoSelectWrapper::getConditionIntRange() const { const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); const std::pair InvalidRange(IntMax, IntMin); int value = std::get(mConstSelect.mValue); switch (getRelationType()) { case ESM::DialogueCondition::Comp_Eq: case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); case ESM::DialogueCondition::Comp_Gt: if (value == IntMax) { return InvalidRange; } else { return std::pair(value + 1, IntMax); } break; case ESM::DialogueCondition::Comp_Ge: return std::pair(value, IntMax); case ESM::DialogueCondition::Comp_Ls: if (value == IntMin) { return InvalidRange; } else { return std::pair(IntMin, value - 1); } case ESM::DialogueCondition::Comp_Le: return std::pair(IntMin, value); default: throw std::logic_error("InfoSelectWrapper: relation does not have a range"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getConditionFloatRange() const { const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); const float Epsilon = std::numeric_limits::epsilon(); float value = std::get(mConstSelect.mValue); switch (getRelationType()) { case ESM::DialogueCondition::Comp_Eq: case ESM::DialogueCondition::Comp_Ne: return std::pair(value, value); case ESM::DialogueCondition::Comp_Gt: return std::pair(value + Epsilon, FloatMax); case ESM::DialogueCondition::Comp_Ge: return std::pair(value, FloatMax); case ESM::DialogueCondition::Comp_Ls: return std::pair(FloatMin, value - Epsilon); case ESM::DialogueCondition::Comp_Le: return std::pair(FloatMin, value); default: throw std::logic_error("InfoSelectWrapper: given relation does not have a range"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getValidIntRange() const { const int IntMax = std::numeric_limits::max(); const int IntMin = std::numeric_limits::min(); switch (getFunctionName()) { // Boolean 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 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 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 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 ESM::DialogueCondition::Function_Weather: return std::pair(0, 9); case ESM::DialogueCondition::Function_FriendHit: return std::pair(0, 4); case ESM::DialogueCondition::Function_RankRequirement: return std::pair(0, 3); case ESM::DialogueCondition::Function_CreatureTarget: return std::pair(0, 2); case ESM::DialogueCondition::Function_PcGender: return std::pair(0, 1); case ESM::DialogueCondition::Function_FactionRankDifference: return std::pair(-9, 9); // Numeric case ESM::DialogueCondition::Function_Global: case ESM::DialogueCondition::Function_Local: case ESM::DialogueCondition::Function_NotLocal: return std::pair(IntMin, IntMax); case ESM::DialogueCondition::Function_PcMagicka: case ESM::DialogueCondition::Function_PcFatigue: case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, IntMax); case ESM::DialogueCondition::Function_Health_Percent: case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: throw std::runtime_error("InfoSelectWrapper: function does not exist"); } } std::pair CSMWorld::ConstInfoSelectWrapper::getValidFloatRange() const { const float FloatMax = std::numeric_limits::infinity(); const float FloatMin = -std::numeric_limits::infinity(); switch (getFunctionName()) { // Numeric case ESM::DialogueCondition::Function_Global: case ESM::DialogueCondition::Function_Local: case ESM::DialogueCondition::Function_NotLocal: return std::pair(FloatMin, FloatMax); case ESM::DialogueCondition::Function_PcMagicka: case ESM::DialogueCondition::Function_PcFatigue: case ESM::DialogueCondition::Function_PcHealth: return std::pair(0, FloatMax); case ESM::DialogueCondition::Function_Health_Percent: case ESM::DialogueCondition::Function_PcHealthPercent: return std::pair(0, 100); default: throw std::runtime_error("InfoSelectWrapper: function does not exist or is not numeric"); } } template 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 { return (containingRange.first <= testRange.first) && (testRange.second <= containingRange.second); } template 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); } template bool CSMWorld::ConstInfoSelectWrapper::rangesMatch(std::pair range1, std::pair range2) const { return (range1.first == range2.first && range1.second == range2.second); } template bool CSMWorld::ConstInfoSelectWrapper::conditionIsAlwaysTrue( std::pair conditionRange, std::pair validRange) const { switch (getRelationType()) { case ESM::DialogueCondition::Comp_Eq: return false; case ESM::DialogueCondition::Comp_Ne: // If value is not within range, it will always be true return !rangeContains(conditionRange.first, validRange); 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); default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } return false; } template bool CSMWorld::ConstInfoSelectWrapper::conditionIsNeverTrue( std::pair conditionRange, std::pair validRange) const { switch (getRelationType()) { case ESM::DialogueCondition::Comp_Eq: return !rangeContains(conditionRange.first, validRange); case ESM::DialogueCondition::Comp_Ne: return false; 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); default: throw std::logic_error("InfoCondition: operator can not be used to compare"); } return false; } QVariant CSMWorld::ConstInfoSelectWrapper::getValue() const { return std::visit([](auto value) { return QVariant(value); }, mConstSelect.mValue); } // InfoSelectWrapper CSMWorld::InfoSelectWrapper::InfoSelectWrapper(ESM::DialogueCondition& select) : CSMWorld::ConstInfoSelectWrapper(select) , mSelect(select) { } void CSMWorld::InfoSelectWrapper::setFunctionName(ESM::DialogueCondition::Function 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(ESM::DialogueCondition::Comparison type) { mSelect.mComparison = type; } void CSMWorld::InfoSelectWrapper::setVariableName(const std::string& name) { mSelect.mVariable = name; } void CSMWorld::InfoSelectWrapper::setValue(int value) { mSelect.mValue = value; } void CSMWorld::InfoSelectWrapper::setValue(float value) { mSelect.mValue = value; } openmw-openmw-0.49.0/apps/opencs/model/world/infoselectwrapper.hpp000066400000000000000000000060271503074453300253530ustar00rootroot00000000000000#ifndef CSM_WORLD_INFOSELECTWRAPPER_H #define CSM_WORLD_INFOSELECTWRAPPER_H #include #include #include #include #include namespace CSMWorld { class ConstInfoSelectWrapper { public: enum ComparisonType { Comparison_Boolean, Comparison_Integer, Comparison_Numeric, Comparison_None }; static const char* FunctionEnumStrings[]; static const char* RelationEnumStrings[]; ConstInfoSelectWrapper(const ESM::DialogueCondition& select); ESM::DialogueCondition::Function getFunctionName() const; ESM::DialogueCondition::Comparison getRelationType() const; ComparisonType getComparisonType() const; bool hasVariable() const; const std::string& getVariableName() const; bool conditionIsAlwaysTrue() const; bool conditionIsNeverTrue() const; QVariant getValue() const; std::string toString() const; protected: void updateHasVariable(); void updateComparisonType(); std::pair getConditionIntRange() const; std::pair getConditionFloatRange() const; std::pair getValidIntRange() const; std::pair getValidFloatRange() const; template bool rangeContains(Type1 value, std::pair range) const; template bool rangesOverlap(std::pair range1, std::pair range2) const; template bool rangeFullyContains(std::pair containing, std::pair test) const; template bool rangesMatch(std::pair range1, std::pair range2) const; template bool conditionIsAlwaysTrue(std::pair conditionRange, std::pair validRange) const; template bool conditionIsNeverTrue(std::pair conditionRange, std::pair validRange) const; ComparisonType mComparisonType; bool mHasVariable; private: const ESM::DialogueCondition& mConstSelect; }; // Wrapper for DialogueCondition that can modify the wrapped select struct class InfoSelectWrapper : public ConstInfoSelectWrapper { public: InfoSelectWrapper(ESM::DialogueCondition& select); // Wrapped SelectStruct will not be modified until update() is called void setFunctionName(ESM::DialogueCondition::Function name); void setRelationType(ESM::DialogueCondition::Comparison type); void setVariableName(const std::string& name); void setValue(int value); void setValue(float value); private: ESM::DialogueCondition& mSelect; void writeRule(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/infotableproxymodel.cpp000066400000000000000000000073451503074453300257040ustar00rootroot00000000000000#include "infotableproxymodel.hpp" #include #include #include #include #include #include #include #include "columns.hpp" #include "idtablebase.hpp" namespace { 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) { Q_ASSERT(type == UniversalId::Type_TopicInfos || type == UniversalId::Type_JournalInfos); } void CSMWorld::InfoTableProxyModel::setSourceModel(QAbstractItemModel* sourceModel) { IdTableProxyModel::setSourceModel(sourceModel); if (mSourceModel != nullptr) { mInfoColumnIndex = mSourceModel->findColumnIndex(mInfoColumnId); mFirstRowCache.clear(); } } bool CSMWorld::InfoTableProxyModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { Q_ASSERT(mSourceModel != nullptr); QModelIndex first = mSourceModel->index(getFirstInfoRow(left.row()), left.column()); QModelIndex second = mSourceModel->index(getFirstInfoRow(right.row()), right.column()); // If both indexes are belonged to the same Topic/Journal, compare their original rows only if (first.row() == second.row()) { return sortOrder() == Qt::AscendingOrder ? left.row() < right.row() : right.row() < left.row(); } return IdTableProxyModel::lessThan(first, second); } int CSMWorld::InfoTableProxyModel::getFirstInfoRow(int currentRow) const { Q_ASSERT(mSourceModel != nullptr); int row = currentRow; int column = mInfoColumnIndex; QString info = toLower(mSourceModel->data(mSourceModel->index(row, column)).toString()); if (mFirstRowCache.contains(info)) { return mFirstRowCache[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*/) { refreshFilter(); mFirstRowCache.clear(); } void CSMWorld::InfoTableProxyModel::sourceRowsInserted(const QModelIndex& parent, int /*start*/, int end) { refreshFilter(); if (!parent.isValid()) { mFirstRowCache.clear(); // We can't re-sort the model here, because the topic of the added row isn't set yet. // Store the row index for using in the first dataChanged() after this row insertion. mLastAddedSourceRow = end; } } void CSMWorld::InfoTableProxyModel::sourceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { refreshFilter(); if (mLastAddedSourceRow != -1 && topLeft.row() <= mLastAddedSourceRow && bottomRight.row() >= mLastAddedSourceRow) { // 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 emit rowAdded(getRecordId(mLastAddedSourceRow).toUtf8().constData()); // Make sure that we perform a re-sorting only in the first dataChanged() after a row insertion mLastAddedSourceRow = -1; } } openmw-openmw-0.49.0/apps/opencs/model/world/infotableproxymodel.hpp000066400000000000000000000025361503074453300257060ustar00rootroot00000000000000#ifndef CSM_WORLD_INFOTABLEPROXYMODEL_HPP #define CSM_WORLD_INFOTABLEPROXYMODEL_HPP #include #include "columns.hpp" #include "idtableproxymodel.hpp" #include "universalid.hpp" class QAbstractItemModel; class QModelIndex; class QObject; namespace CSMWorld { class InfoTableProxyModel : public IdTableProxyModel { Q_OBJECT UniversalId::Type mType; Columns::ColumnId mInfoColumnId; ///< Contains ID for Topic or Journal ID int mInfoColumnIndex; int mLastAddedSourceRow; 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. public: InfoTableProxyModel(UniversalId::Type type, QObject* parent = nullptr); void setSourceModel(QAbstractItemModel* sourceModel) 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; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/land.cpp000066400000000000000000000015071503074453300225260ustar00rootroot00000000000000#include "land.hpp" #include #include #include namespace ESM { class ESMReader; } namespace CSMWorld { void Land::load(ESM::ESMReader& esm, bool& isDeleted) { ESM::Land::load(esm, isDeleted); } std::string Land::createUniqueRecordId(int x, int y) { std::ostringstream stream; stream << "#" << x << " " << y; return stream.str(); } void Land::parseUniqueRecordId(const std::string& id, int& x, int& y) { size_t mid = id.find(' '); if (mid == std::string::npos || id[0] != '#') throw std::runtime_error("Invalid Land ID"); x = Misc::StringUtils::toNumeric(id.substr(1, mid - 1), 0); y = Misc::StringUtils::toNumeric(id.substr(mid + 1), 0); } } openmw-openmw-0.49.0/apps/opencs/model/world/land.hpp000066400000000000000000000011441503074453300225300ustar00rootroot00000000000000#ifndef CSM_WORLD_LAND_H #define CSM_WORLD_LAND_H #include #include namespace ESM { class ESMReader; } namespace CSMWorld { /// \brief Wrapper for Land record. Encodes X and Y cell index in the ID. /// /// \todo Add worldspace support to the Land record. struct Land : public ESM::Land { /// Loads the metadata and ID 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/landtexturetableproxymodel.cpp000066400000000000000000000006761503074453300273100ustar00rootroot00000000000000#include "landtexturetableproxymodel.hpp" #include namespace CSMWorld { LandTextureTableProxyModel::LandTextureTableProxyModel(QObject* parent) : IdTableProxyModel(parent) { } bool LandTextureTableProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const { return IdTableProxyModel::filterAcceptsRow(sourceRow, sourceParent); } } openmw-openmw-0.49.0/apps/opencs/model/world/landtexturetableproxymodel.hpp000066400000000000000000000010251503074453300273020ustar00rootroot00000000000000#ifndef CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H #define CSM_WORLD_LANDTEXTURETABLEPROXYMODEL_H #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: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/metadata.cpp000066400000000000000000000014461503074453300233720ustar00rootroot00000000000000#include "metadata.hpp" #include #include #include void CSMWorld::MetaData::blank() { // 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) { mFormatVersion = esm.getHeader().mFormatVersion; mAuthor = esm.getHeader().mData.author; mDescription = esm.getHeader().mData.desc; } void CSMWorld::MetaData::save(ESM::ESMWriter& esm) const { esm.setFormatVersion(mFormatVersion); esm.setAuthor(mAuthor); esm.setDescription(mDescription); } openmw-openmw-0.49.0/apps/opencs/model/world/metadata.hpp000066400000000000000000000011401503074453300233660ustar00rootroot00000000000000#ifndef CSM_WOLRD_METADATA_H #define CSM_WOLRD_METADATA_H #include #include #include namespace ESM { class ESMReader; class ESMWriter; } namespace CSMWorld { struct MetaData { static constexpr std::string_view getRecordType() { return "MetaData"; } ESM::RefId mId; ESM::FormatVersion mFormatVersion; std::string mAuthor; std::string mDescription; void blank(); void load(ESM::ESMReader& esm); void save(ESM::ESMWriter& esm) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/nestedcoladapterimp.cpp000066400000000000000000001163131503074453300256410ustar00rootroot00000000000000#include "nestedcoladapterimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "info.hpp" #include "infoselectwrapper.hpp" #include "pathgrid.hpp" namespace CSMWorld { void PathgridPointListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::PointList& points = pathgrid.mPoints; // blank row ESM::Pathgrid::Point point; point.mX = 0; point.mY = 0; point.mZ = 0; point.mAutogenerated = 0; point.mConnectionNum = 0; points.insert(points.begin() + position, point); pathgrid.mData.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } void PathgridPointListAdapter::removeRow(Record& record, int rowToRemove) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::PointList& points = pathgrid.mPoints; 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.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } void PathgridPointListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); pathgrid.mPoints = static_cast&>(nestedTable).mNestedTable; pathgrid.mData.mPoints = pathgrid.mPoints.size(); record.setModified(pathgrid); } NestedTableWrapperBase* PathgridPointListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper(record.get().mPoints); } 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"); } } 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"); } pathgrid.mPoints[subRowIndex] = point; record.setModified(pathgrid); } int PathgridPointListAdapter::getColumnsCount(const Record& record) const { return 4; } int PathgridPointListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mPoints.size()); } void PathgridEdgeListAdapter::addRow(Record& record, int position) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges; // blank row ESM::Pathgrid::Edge edge; edge.mV0 = 0; edge.mV1 = 0; // NOTE: inserting a blank edge does not really make sense, perhaps this should be a // logic_error exception // // 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); record.setModified(pathgrid); } void PathgridEdgeListAdapter::removeRow(Record& record, int rowToRemove) const { Pathgrid pathgrid = record.get(); ESM::Pathgrid::EdgeList& edges = pathgrid.mEdges; if (rowToRemove < 0 || rowToRemove >= static_cast(edges.size())) throw std::runtime_error("index out of range"); edges.erase(edges.begin() + rowToRemove); record.setModified(pathgrid); } void PathgridEdgeListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Pathgrid pathgrid = record.get(); pathgrid.mEdges = static_cast&>(nestedTable).mNestedTable; record.setModified(pathgrid); } NestedTableWrapperBase* PathgridEdgeListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper(record.get().mEdges); } 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"); ESM::Pathgrid::Edge edge = pathgrid.mEdges[subRowIndex]; switch (subColIndex) { 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 { Pathgrid pathgrid = record.get(); 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"); } pathgrid.mEdges[subRowIndex] = edge; record.setModified(pathgrid); } int PathgridEdgeListAdapter::getColumnsCount(const Record& record) const { return 3; } int PathgridEdgeListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mEdges.size()); } void FactionReactionsAdapter::addRow(Record& record, int position) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; // blank row reactions.insert(std::make_pair(ESM::RefId(), 0)); record.setModified(faction); } void FactionReactionsAdapter::removeRow(Record& record, int rowToRemove) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; 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 auto iter = reactions.begin(); for (int i = 0; i < rowToRemove; ++i) ++iter; reactions.erase(iter); record.setModified(faction); } void FactionReactionsAdapter::setTable( Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Faction faction = record.get(); faction.mReactions = static_cast>&>(nestedTable).mNestedTable; record.setModified(faction); } NestedTableWrapperBase* FactionReactionsAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mReactions); } QVariant FactionReactionsAdapter::getData( const Record& record, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; 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 auto iter = reactions.begin(); for (int i = 0; i < subRowIndex; ++i) ++iter; switch (subColIndex) { 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 { ESM::Faction faction = record.get(); std::map& reactions = faction.mReactions; 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 auto iter = reactions.begin(); for (int i = 0; i < subRowIndex; ++i) ++iter; ESM::RefId factionId = (*iter).first; int reaction = (*iter).second; switch (subColIndex) { case 0: { reactions.erase(iter); reactions.insert( std::make_pair(ESM::RefId::stringRefId(value.toString().toUtf8().constData()), reaction)); break; } case 1: { reactions[factionId] = value.toInt(); break; } default: throw std::runtime_error("Faction reactions subcolumn index out of range"); } record.setModified(faction); } int FactionReactionsAdapter::getColumnsCount(const Record& record) const { return 2; } int FactionReactionsAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mReactions.size()); } void RegionSoundListAdapter::addRow(Record& record, int position) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; // blank row ESM::Region::SoundRef soundRef; soundRef.mSound = ESM::RefId(); soundRef.mChance = 0; soundList.insert(soundList.begin() + position, soundRef); record.setModified(region); } void RegionSoundListAdapter::removeRow(Record& record, int rowToRemove) const { ESM::Region region = record.get(); std::vector& soundList = region.mSoundList; if (rowToRemove < 0 || rowToRemove >= static_cast(soundList.size())) throw std::runtime_error("index out of range"); soundList.erase(soundList.begin() + rowToRemove); record.setModified(region); } void RegionSoundListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Region region = record.get(); region.mSoundList = static_cast>&>(nestedTable).mNestedTable; record.setModified(region); } NestedTableWrapperBase* RegionSoundListAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mSoundList); } QVariant RegionSoundListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { const ESM::Region& region = record.get(); const std::vector& soundList = region.mSoundList; const size_t index = static_cast(subRowIndex); if (subRowIndex < 0 || index >= soundList.size()) throw std::runtime_error("index out of range"); const ESM::Region::SoundRef& soundRef = soundList[subRowIndex]; switch (subColIndex) { 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 { 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"); ESM::Region::SoundRef soundRef = soundList[subRowIndex]; switch (subColIndex) { 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); } int RegionSoundListAdapter::getColumnsCount(const Record& record) const { return 3; } int RegionSoundListAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mSoundList.size()); } void InfoListAdapter::addRow(Record& record, int position) const { 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"); } void InfoListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* InfoListAdapter::table(const Record& record) const { throw std::logic_error("table operation not supported"); } QVariant InfoListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); if (subColIndex == 0) return QString(info.mResultScript.c_str()); else 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 { Info info = record.get(); if (subColIndex == 0) info.mResultScript = value.toString().toStdString(); else throw std::runtime_error("Trying to access non-existing column in the nested table!"); record.setModified(info); } int InfoListAdapter::getColumnsCount(const Record& record) const { return 1; } int InfoListAdapter::getRowsCount(const Record& record) const { return 1; // fixed at size 1 } void InfoConditionAdapter::addRow(Record& record, int position) const { Info info = record.get(); auto& conditions = info.mSelects; // default row ESM::DialogueCondition condStruct; condStruct.mIndex = conditions.size(); conditions.insert(conditions.begin() + position, condStruct); record.setModified(info); } void InfoConditionAdapter::removeRow(Record& record, int rowToRemove) const { Info info = record.get(); auto& conditions = info.mSelects; if (rowToRemove < 0 || rowToRemove >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); conditions.erase(conditions.begin() + rowToRemove); record.setModified(info); } void InfoConditionAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { Info info = record.get(); info.mSelects = static_cast>&>(nestedTable).mNestedTable; record.setModified(info); } NestedTableWrapperBase* InfoConditionAdapter::table(const Record& record) const { // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mSelects); } QVariant InfoConditionAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { Info info = record.get(); auto& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); ConstInfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); switch (subColIndex) { case 0: { return infoSelectWrapper.getFunctionName(); } case 1: { if (infoSelectWrapper.hasVariable()) return QString(infoSelectWrapper.getVariableName().c_str()); else return ""; } case 2: { return infoSelectWrapper.getRelationType() - ESM::DialogueCondition::Comp_Eq; } case 3: { return infoSelectWrapper.getValue(); } 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 { Info info = record.get(); auto& conditions = info.mSelects; if (subRowIndex < 0 || subRowIndex >= static_cast(conditions.size())) throw std::runtime_error("index out of range"); InfoSelectWrapper infoSelectWrapper(conditions[subRowIndex]); bool conversionResult = false; switch (subColIndex) { case 0: // Function { infoSelectWrapper.setFunctionName(static_cast(value.toInt())); break; } case 1: // Variable { infoSelectWrapper.setVariableName(value.toString().toUtf8().constData()); break; } case 2: // Relation { infoSelectWrapper.setRelationType( static_cast(value.toInt() + ESM::DialogueCondition::Comp_Eq)); break; } case 3: // Value { switch (infoSelectWrapper.getComparisonType()) { case ConstInfoSelectWrapper::Comparison_Numeric: { // QVariant seems to have issues converting 0 if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { infoSelectWrapper.setValue(value.toInt()); } else if (value.toFloat(&conversionResult) && conversionResult) { infoSelectWrapper.setValue(value.toFloat()); } break; } case ConstInfoSelectWrapper::Comparison_Boolean: case ConstInfoSelectWrapper::Comparison_Integer: { if ((value.toInt(&conversionResult) && conversionResult) || value.toString().compare("0") == 0) { infoSelectWrapper.setValue(value.toInt()); } break; } default: break; } break; } default: throw std::runtime_error("Info condition subcolumn index out of range"); } record.setModified(info); } int InfoConditionAdapter::getColumnsCount(const Record& record) const { return 4; } int InfoConditionAdapter::getRowsCount(const Record& record) const { return static_cast(record.get().mSelects.size()); } void RaceAttributeAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user } void RaceAttributeAdapter::removeRow(Record& record, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void RaceAttributeAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); race.mData = static_cast>&>(nestedTable) .mNestedTable.at(0); record.setModified(race); } NestedTableWrapperBase* RaceAttributeAdapter::table(const Record& record) const { std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } QVariant RaceAttributeAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); 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.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 { ESM::Race race = record.get(); 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.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); } int RaceAttributeAdapter::getColumnsCount(const Record& record) const { return 3; // attrib, male, female } int RaceAttributeAdapter::getRowsCount(const Record& record) const { return ESM::Attribute::Length; // there are 8 attributes } void RaceSkillsBonusAdapter::addRow(Record& record, int position) const { // Do nothing, this table cannot be changed by the user } void RaceSkillsBonusAdapter::removeRow(Record& record, int rowToRemove) const { // Do nothing, this table cannot be changed by the user } void RaceSkillsBonusAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { ESM::Race race = record.get(); race.mData = static_cast>&>(nestedTable) .mNestedTable.at(0); record.setModified(race); } NestedTableWrapperBase* RaceSkillsBonusAdapter::table(const Record& record) const { std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } QVariant RaceSkillsBonusAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); 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"); } } void RaceSkillsBonusAdapter::setData( Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Race race = record.get(); 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"); } record.setModified(race); } int RaceSkillsBonusAdapter::getColumnsCount(const Record& record) const { return 2; // skill, bonus } int RaceSkillsBonusAdapter::getRowsCount(const Record& record) const { return record.get().mData.mBonus.size(); } void CellListAdapter::addRow(Record& record, int position) const { 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"); } void CellListAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* CellListAdapter::table(const Record& record) const { throw std::logic_error("table operation not supported"); } QVariant CellListAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { CSMWorld::Cell cell = record.get(); bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0; bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0; switch (subColIndex) { 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 : 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 DisableTag::getVariant(); } 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 { CSMWorld::Cell cell = record.get(); bool isInterior = (cell.mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (cell.mData.mFlags & ESM::Cell::QuasiEx) != 0; bool interiorWater = (cell.mData.mFlags & ESM::Cell::HasWater) != 0; switch (subColIndex) { case 0: { if (value.toBool()) cell.mData.mFlags |= ESM::Cell::Interior; else cell.mData.mFlags &= ~ESM::Cell::Interior; break; } case 1: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mAmbient = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 2: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mSunlight = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 3: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mFog = static_cast(value.toInt()); cell.setHasAmbient(true); } else return; // return without saving break; } case 4: { if (isInterior && !behaveLikeExterior) { cell.mAmbi.mFogDensity = value.toFloat(); cell.setHasAmbient(true); } else return; // return without saving break; } case 5: { if (isInterior && interiorWater) { cell.mWater = value.toFloat(); cell.setHasWaterHeightSub(true); } else return; // return without saving break; } case 6: { if (!isInterior) cell.mMapColor = value.toInt(); else return; // return without saving break; } #if 0 // redundant since this flag is shown in the main table as "Interior Sky" // keep here for documenting the logic based on vanilla case 7: { if (isInterior) { if (value.toBool()) cell.mData.mFlags |= ESM::Cell::QuasiEx; else cell.mData.mFlags &= ~ESM::Cell::QuasiEx; } else return; // return without saving break; } #endif default: throw std::runtime_error("Cell subcolumn index out of range"); } record.setModified(cell); } int CellListAdapter::getColumnsCount(const Record& record) const { return 7; } int CellListAdapter::getRowsCount(const Record& record) const { return 1; // fixed at size 1 } void RegionWeatherAdapter::addRow(Record& record, int position) const { 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"); } void RegionWeatherAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* RegionWeatherAdapter::table(const Record& record) const { 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 ESM::Region& region = record.get(); if (subColIndex == 0 && subRowIndex >= 0 && subRowIndex < 10) { return WeatherNames[subRowIndex]; } else if (subColIndex == 1) { 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 { ESM::Region region = record.get(); uint8_t chance = static_cast(value.toInt()); if (subColIndex == 1) { region.mData.mProbabilities.at(subRowIndex) = chance; record.setModified(region); } } int RegionWeatherAdapter::getColumnsCount(const Record& record) const { return 2; } int RegionWeatherAdapter::getRowsCount(const Record& record) const { return 10; } void FactionRanksAdapter::addRow(Record& record, int position) const { 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"); } void FactionRanksAdapter::setTable(Record& record, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* FactionRanksAdapter::table(const Record& record) const { throw std::logic_error("table operation not supported"); } QVariant FactionRanksAdapter::getData(const Record& record, int subRowIndex, int subColIndex) const { const ESM::Faction& faction = record.get(); 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"); } } void FactionRanksAdapter::setData( Record& record, const QVariant& value, int subRowIndex, int subColIndex) const { ESM::Faction faction = record.get(); 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"); } record.setModified(faction); } int FactionRanksAdapter::getColumnsCount(const Record& record) const { return 6; } int FactionRanksAdapter::getRowsCount(const Record& record) const { return 10; } } openmw-openmw-0.49.0/apps/opencs/model/world/nestedcoladapterimp.hpp000066400000000000000000000517771503074453300256620ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDCOLADAPTERIMP_H #define CSM_WOLRD_NESTEDCOLADAPTERIMP_H #include #include #include #include #include #include #include #include // for converting magic effect id to string & back #include "nestedcolumnadapter.hpp" #include "nestedtablewrapper.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() = 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class PathgridEdgeListAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class FactionReactionsAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class FactionRanksAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RegionSoundListAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; template class SpellListAdapter : public NestedColumnAdapter { public: SpellListAdapter() = default; void addRow(Record& record, int position) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; // blank row ESM::RefId spell; spells.insert(spells.begin() + position, spell); record.setModified(raceOrBthSgn); } void removeRow(Record& record, int rowToRemove) const override { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; if (rowToRemove < 0 || rowToRemove >= static_cast(spells.size())) throw std::runtime_error("index out of range"); spells.erase(spells.begin() + rowToRemove); record.setModified(raceOrBthSgn); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT raceOrBthSgn = record.get(); raceOrBthSgn.mPowers.mList = static_cast>&>(nestedTable).mNestedTable; record.setModified(raceOrBthSgn); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring 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; if (subRowIndex < 0 || subRowIndex >= static_cast(spells.size())) throw std::runtime_error("index out of range"); ESM::RefId spell = spells[subRowIndex]; switch (subColIndex) { 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 { ESXRecordT raceOrBthSgn = record.get(); std::vector& spells = raceOrBthSgn.mPowers.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(spells.size())) throw std::runtime_error("index out of range"); ESM::RefId spell = spells[subRowIndex]; switch (subColIndex) { 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); } int getColumnsCount(const Record& record) const override { return 1; } int getRowsCount(const Record& record) const override { return static_cast(record.get().mPowers.mList.size()); } }; template class EffectsListAdapter : public NestedColumnAdapter { public: EffectsListAdapter() = default; void addRow(Record& record, int position) const override { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; // blank row 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; if (rowToRemove < 0 || rowToRemove >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); effectsList.erase(effectsList.begin() + rowToRemove); magic.mEffects.updateIndexes(); record.setModified(magic); } void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const override { ESXRecordT magic = record.get(); magic.mEffects.mList = static_cast>&>(nestedTable).mNestedTable; record.setModified(magic); } NestedTableWrapperBase* table(const Record& record) const override { // deleted by dtor of NestedTableStoring 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; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); ESM::ENAMstruct effect = effectsList[subRowIndex].mData; switch (subColIndex) { case 0: return effect.mEffectID; case 1: { switch (effect.mEffectID) { case ESM::MagicEffect::DrainSkill: case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::RestoreSkill: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::AbsorbSkill: return effect.mSkill; default: return QVariant(); } } case 2: { switch (effect.mEffectID) { case ESM::MagicEffect::DrainAttribute: case ESM::MagicEffect::DamageAttribute: case ESM::MagicEffect::RestoreAttribute: case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::AbsorbAttribute: return effect.mAttribute; default: return QVariant(); } } case 3: 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 { ESXRecordT magic = record.get(); std::vector& effectsList = magic.mEffects.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(effectsList.size())) throw std::runtime_error("index out of range"); 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: { effect.mSkill = static_cast(value.toInt()); break; } case 2: { effect.mAttribute = static_cast(value.toInt()); break; } case 3: { 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"); } magic.mEffects.mList[subRowIndex].mData = effect; record.setModified(magic); } int getColumnsCount(const Record& record) const override { return 8; } int getRowsCount(const Record& record) const override { return static_cast(record.get().mEffects.mList.size()); } }; class InfoListAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class InfoConditionAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RaceAttributeAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RaceSkillsBonusAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class CellListAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; class RegionWeatherAdapter : public NestedColumnAdapter { public: 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; NestedTableWrapperBase* table(const Record& record) 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; int getColumnsCount(const Record& record) const override; int getRowsCount(const Record& record) const override; }; } #endif // CSM_WOLRD_NESTEDCOLADAPTERIMP_H openmw-openmw-0.49.0/apps/opencs/model/world/nestedcollection.cpp000066400000000000000000000021001503074453300251340ustar00rootroot00000000000000#include "nestedcollection.hpp" #include #include #include "columnbase.hpp" int CSMWorld::NestedCollection::getNestedRowsCount(int row, int column) const { return 0; } int CSMWorld::NestedCollection::getNestedColumnsCount(int row, int column) const { return 0; } int CSMWorld::NestedCollection::searchNestedColumnIndex(int parentColumn, Columns::ColumnId id) { // Assumed that the parentColumn is always a valid index const NestableColumn* parent = getNestableColumn(parentColumn); int nestedColumnCount = getNestedColumnsCount(0, parentColumn); for (int i = 0; i < nestedColumnCount; ++i) { if (parent->nestedColumn(i).mColumnId == id) { return i; } } return -1; } int CSMWorld::NestedCollection::findNestedColumnIndex(int parentColumn, Columns::ColumnId id) { int index = searchNestedColumnIndex(parentColumn, id); if (index == -1) { throw std::logic_error("CSMWorld::NestedCollection: No such nested column"); } return index; } openmw-openmw-0.49.0/apps/opencs/model/world/nestedcollection.hpp000066400000000000000000000027171503074453300251570ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDCOLLECTION_H #define CSM_WOLRD_NESTEDCOLLECTION_H #include "columns.hpp" class QVariant; namespace CSMWorld { class NestableColumn; struct NestedTableWrapperBase; class NestedCollection { public: NestedCollection() = default; virtual ~NestedCollection() = default; virtual void addNestedRow(int row, int col, int position) = 0; virtual void removeNestedRows(int row, int column, int subRow) = 0; virtual QVariant getNestedData(int row, int column, int subRow, int subColumn) const = 0; virtual void setNestedData(int row, int column, const QVariant& data, int subRow, int subColumn) = 0; virtual NestedTableWrapperBase* nestedTable(int row, int column) const = 0; virtual void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) = 0; virtual int getNestedRowsCount(int row, int column) const; virtual int getNestedColumnsCount(int row, int column) const; 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. virtual int findNestedColumnIndex(int parentColumn, Columns::ColumnId id); ///< \return the column index or throws an exception if the requested column wasn't found. }; } #endif // CSM_WOLRD_NESTEDCOLLECTION_H openmw-openmw-0.49.0/apps/opencs/model/world/nestedcolumnadapter.hpp000066400000000000000000000023111503074453300256500ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDCOLUMNADAPTER_H #define CSM_WOLRD_NESTEDCOLUMNADAPTER_H class QVariant; namespace CSMWorld { struct NestedTableWrapperBase; template struct Record; template class NestedColumnAdapter { public: NestedColumnAdapter() = default; virtual ~NestedColumnAdapter() = default; virtual void addRow(Record& record, int position) const = 0; virtual void removeRow(Record& record, int rowToRemove) const = 0; virtual void setTable(Record& record, const NestedTableWrapperBase& nestedTable) const = 0; virtual NestedTableWrapperBase* table(const Record& record) const = 0; 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 int getColumnsCount(const Record& record) const = 0; virtual int getRowsCount(const Record& record) const = 0; }; } #endif // CSM_WOLRD_NESTEDCOLUMNADAPTER_H openmw-openmw-0.49.0/apps/opencs/model/world/nestedidcollection.hpp000066400000000000000000000146541503074453300254770ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDIDCOLLECTION_H #define CSM_WOLRD_NESTEDIDCOLLECTION_H #include #include #include #include "collection.hpp" #include "nestedcollection.hpp" #include "nestedcolumnadapter.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct NestedTableWrapperBase; struct Cell; struct ColumnBase; template class IdCollection; template class NestedColumnAdapter; template class NestedIdCollection : public IdCollection, public NestedCollection { std::map*> mAdapters; const NestedColumnAdapter& getAdapter(const ColumnBase& column) const; public: NestedIdCollection(); ~NestedIdCollection() override; void addNestedRow(int row, int column, int position) override; void removeNestedRows(int row, int column, int subRow) 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; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(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; void addAdapter(std::pair*> adapter); }; template NestedIdCollection::NestedIdCollection() { } template NestedIdCollection::~NestedIdCollection() { for (typename std::map*>::iterator iter(mAdapters.begin()); iter != mAdapters.end(); ++iter) { delete (*iter).second; } } template void NestedIdCollection::addAdapter( std::pair*> adapter) { mAdapters.insert(adapter); } template const NestedColumnAdapter& NestedIdCollection::getAdapter(const ColumnBase& column) const { typename std::map*>::const_iterator iter = mAdapters.find(&column); 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) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).addRow(*record, position); Collection::setRecord(row, std::move(record)); } template void NestedIdCollection::removeNestedRows(int row, int column, int subRow) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); Collection::setRecord(row, std::move(record)); } template QVariant NestedIdCollection::getNestedData(int row, int column, int subRow, int subColumn) const { 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) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setData(*record, data, subRow, subColumn); Collection::setRecord(row, std::move(record)); } template CSMWorld::NestedTableWrapperBase* NestedIdCollection::nestedTable(int row, int column) const { return getAdapter(Collection::getColumn(column)).table(Collection::getRecord(row)); } template void NestedIdCollection::setNestedTable( int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setTable(*record, nestedTable); Collection::setRecord(row, std::move(record)); } template int NestedIdCollection::getNestedRowsCount(int row, int column) const { return getAdapter(Collection::getColumn(column)) .getRowsCount(Collection::getRecord(row)); } template int NestedIdCollection::getNestedColumnsCount(int row, int column) const { const ColumnBase& nestedColumn = Collection::getColumn(column); int numRecords = Collection::getSize(); if (row >= 0 && row < numRecords) { const Record& record = Collection::getRecord(row); return getAdapter(nestedColumn).getColumnsCount(record); } else { // If the row is invalid (or there no records), retrieve the column count using a blank record const Record record; return getAdapter(nestedColumn).getColumnsCount(record); } } template CSMWorld::NestableColumn* NestedIdCollection::getNestableColumn(int column) { return Collection::getNestableColumn(column); } } #endif // CSM_WOLRD_NESTEDIDCOLLECTION_H openmw-openmw-0.49.0/apps/opencs/model/world/nestedinfocollection.cpp000066400000000000000000000074421503074453300260260ustar00rootroot00000000000000#include "nestedinfocollection.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace CSMWorld { NestedInfoCollection::~NestedInfoCollection() { for (std::map*>::iterator iter(mAdapters.begin()); iter != mAdapters.end(); ++iter) { delete (*iter).second; } } void NestedInfoCollection::addAdapter(std::pair*> adapter) { mAdapters.insert(adapter); } const NestedColumnAdapter& NestedInfoCollection::getAdapter(const ColumnBase& column) const { std::map*>::const_iterator iter = mAdapters.find(&column); if (iter == mAdapters.end()) throw std::logic_error("No such column in the nestedidadapter"); return *iter->second; } void NestedInfoCollection::addNestedRow(int row, int column, int position) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).addRow(*record, position); Collection::setRecord(row, std::move(record)); } void NestedInfoCollection::removeNestedRows(int row, int column, int subRow) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).removeRow(*record, subRow); Collection::setRecord(row, std::move(record)); } QVariant NestedInfoCollection::getNestedData(int row, int column, int subRow, int subColumn) const { 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) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setData(*record, data, subRow, subColumn); Collection::setRecord(row, std::move(record)); } CSMWorld::NestedTableWrapperBase* NestedInfoCollection::nestedTable(int row, int column) const { return getAdapter(Collection::getColumn(column)).table(Collection::getRecord(row)); } void NestedInfoCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { auto record = std::make_unique>(); record->assign(Collection::getRecord(row)); getAdapter(Collection::getColumn(column)).setTable(*record, nestedTable); Collection::setRecord(row, std::move(record)); } int NestedInfoCollection::getNestedRowsCount(int row, int column) const { 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)); } CSMWorld::NestableColumn* NestedInfoCollection::getNestableColumn(int column) { return Collection::getNestableColumn(column); } } openmw-openmw-0.49.0/apps/opencs/model/world/nestedinfocollection.hpp000066400000000000000000000032531503074453300260270ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDINFOCOLLECTION_H #define CSM_WOLRD_NESTEDINFOCOLLECTION_H #include #include #include #include "infocollection.hpp" #include "nestedcollection.hpp" namespace CSMWorld { struct NestedTableWrapperBase; class NestableColumn; struct ColumnBase; struct Info; template class NestedColumnAdapter; class NestedInfoCollection : public InfoCollection, public NestedCollection { std::map*> mAdapters; const NestedColumnAdapter& getAdapter(const ColumnBase& column) const; public: NestedInfoCollection() = default; ~NestedInfoCollection() override; void addNestedRow(int row, int column, int position) override; void removeNestedRows(int row, int column, int subRow) 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; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(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; void addAdapter(std::pair*> adapter); }; } #endif // CSM_WOLRD_NESTEDINFOCOLLECTION_H openmw-openmw-0.49.0/apps/opencs/model/world/nestedtableproxymodel.cpp000066400000000000000000000134121503074453300262230ustar00rootroot00000000000000#include "nestedtableproxymodel.hpp" #include "idtree.hpp" #include #include CSMWorld::NestedTableProxyModel::NestedTableProxyModel( const QModelIndex& parent, ColumnBase::Display columnId, CSMWorld::IdTree* parentModel) : mParentColumn(parent.column()) , mMainModel(parentModel) { const int parentRow = parent.row(); mId = std::string(parentModel->index(parentRow, 0).data().toString().toUtf8()); QAbstractProxyModel::setSourceModel(parentModel); connect(mMainModel, &IdTree::rowsAboutToBeInserted, this, &NestedTableProxyModel::forwardRowsAboutToInserted); connect(mMainModel, &IdTree::rowsInserted, this, &NestedTableProxyModel::forwardRowsInserted); connect(mMainModel, &IdTree::rowsAboutToBeRemoved, this, &NestedTableProxyModel::forwardRowsAboutToRemoved); connect(mMainModel, &IdTree::rowsRemoved, this, &NestedTableProxyModel::forwardRowsRemoved); connect(mMainModel, &IdTree::resetStart, this, &NestedTableProxyModel::forwardResetStart); connect(mMainModel, &IdTree::resetEnd, this, &NestedTableProxyModel::forwardResetEnd); 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); if (testedParent == parent) { return createIndex(sourceIndex.row(), sourceIndex.column()); } else { return QModelIndex(); } } QModelIndex CSMWorld::NestedTableProxyModel::mapToSource(const QModelIndex& proxyIndex) const { 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()); return mMainModel->rowCount(mMainModel->getModelIndex(mId, mParentColumn)); } int CSMWorld::NestedTableProxyModel::columnCount(const QModelIndex& parent) const { 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()); int numRows = rowCount(parent); int numColumns = columnCount(parent); if (row < 0 || row >= numRows || column < 0 || column >= numColumns) return QModelIndex(); return createIndex(row, column); } QModelIndex CSMWorld::NestedTableProxyModel::parent(const QModelIndex& index) const { return QModelIndex(); } QVariant CSMWorld::NestedTableProxyModel::headerData(int section, Qt::Orientation orientation, int role) const { return mMainModel->nestedHeaderData(mParentColumn, section, orientation, role); } QVariant CSMWorld::NestedTableProxyModel::data(const QModelIndex& index, int role) const { return mMainModel->data(mapToSource(index), role); } // 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) { return mMainModel->setData(mapToSource(index), value, role); } Qt::ItemFlags CSMWorld::NestedTableProxyModel::flags(const QModelIndex& index) const { return mMainModel->flags(mapToSource(index)); } std::string CSMWorld::NestedTableProxyModel::getParentId() const { return mId; } int CSMWorld::NestedTableProxyModel::getParentColumn() const { return mParentColumn; } CSMWorld::IdTree* CSMWorld::NestedTableProxyModel::model() const { return mMainModel; } void CSMWorld::NestedTableProxyModel::forwardRowsAboutToInserted(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { beginInsertRows(QModelIndex(), first, last); } } void CSMWorld::NestedTableProxyModel::forwardRowsInserted(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { endInsertRows(); } } bool CSMWorld::NestedTableProxyModel::indexIsParent(const QModelIndex& index) { 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) { if (indexIsParent(parent)) { beginRemoveRows(QModelIndex(), first, last); } } void CSMWorld::NestedTableProxyModel::forwardRowsRemoved(const QModelIndex& parent, int first, int last) { if (indexIsParent(parent)) { endRemoveRows(); } } void CSMWorld::NestedTableProxyModel::forwardResetStart(const QString& id) { if (id.toUtf8() == mId.c_str()) beginResetModel(); } void CSMWorld::NestedTableProxyModel::forwardResetEnd(const QString& id) { if (id.toUtf8() == mId.c_str()) endResetModel(); } void CSMWorld::NestedTableProxyModel::forwardDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { 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)); } else if (topLeft.parent() == parent && bottomRight.parent() == parent) { emit dataChanged(index(topLeft.row(), topLeft.column()), index(bottomRight.row(), bottomRight.column())); } } openmw-openmw-0.49.0/apps/opencs/model/world/nestedtableproxymodel.hpp000066400000000000000000000046161503074453300262360ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDTABLEPROXYMODEL_H #define CSM_WOLRD_NESTEDTABLEPROXYMODEL_H #include #include #include #include #include #include "columnbase.hpp" /*! \brief * Proxy model used to connect view in the dialogue into the nested columns of the main model. */ namespace CSMWorld { class IdTree; class NestedTableProxyModel : public QAbstractProxyModel { Q_OBJECT const int mParentColumn; IdTree* mMainModel; 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 std::string getParentId() const; int getParentColumn() const; CSMWorld::IdTree* model() const; QModelIndex mapFromSource(const QModelIndex& sourceIndex) const override; QModelIndex mapToSource(const QModelIndex& proxyIndex) const override; int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex& index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) 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; Qt::ItemFlags flags(const QModelIndex& index) const override; private: void setupHeaderVectors(ColumnBase::Display columnId); bool indexIsParent(const QModelIndex& index); private slots: void forwardRowsAboutToInserted(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 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/nestedtablewrapper.cpp000066400000000000000000000001511503074453300254750ustar00rootroot00000000000000#include "nestedtablewrapper.hpp" int CSMWorld::NestedTableWrapperBase::size() const { return -5; } openmw-openmw-0.49.0/apps/opencs/model/world/nestedtablewrapper.hpp000066400000000000000000000013401503074453300255030ustar00rootroot00000000000000#ifndef CSM_WOLRD_NESTEDTABLEWRAPPER_H #define CSM_WOLRD_NESTEDTABLEWRAPPER_H namespace CSMWorld { struct NestedTableWrapperBase { virtual ~NestedTableWrapperBase() = default; virtual int size() const; NestedTableWrapperBase() = default; }; template struct NestedTableWrapper : public NestedTableWrapperBase { NestedTable mNestedTable; NestedTableWrapper(const NestedTable& nestedTable) : mNestedTable(nestedTable) { } ~NestedTableWrapper() override = default; int size() const override { return mNestedTable.size(); // i hope that this will be enough } }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/pathgrid.cpp000066400000000000000000000014151503074453300234100ustar00rootroot00000000000000#include "pathgrid.hpp" #include "cell.hpp" #include "idcollection.hpp" #include void CSMWorld::Pathgrid::load(ESM::ESMReader& esm, bool& isDeleted, const IdCollection& cells) { load(esm, isDeleted); // correct ID if (!mId.empty() && !mId.startsWith("#") && cells.searchId(mId) == -1) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; mId = ESM::RefId::stringRefId(stream.str()); } } void CSMWorld::Pathgrid::load(ESM::ESMReader& esm, bool& isDeleted) { ESM::Pathgrid::load(esm, isDeleted); mId = mCell; if (mCell.empty()) { std::ostringstream stream; stream << "#" << mData.mX << " " << mData.mY; mId = ESM::RefId::stringRefId(stream.str()); } } openmw-openmw-0.49.0/apps/opencs/model/world/pathgrid.hpp000066400000000000000000000013351503074453300234160ustar00rootroot00000000000000#ifndef CSM_WOLRD_PATHGRID_H #define CSM_WOLRD_PATHGRID_H #include #include #include namespace ESM { class ESMReader; } namespace CSMWorld { struct Cell; template class IdCollection; /// \brief Wrapper for Pathgrid record /// /// \attention The mData.mX and mData.mY fields of the ESM::Pathgrid struct are not used. /// Exterior cell coordinates are encoded in the pathgrid ID. struct Pathgrid : public ESM::Pathgrid { ESM::RefId mId; void load(ESM::ESMReader& esm, bool& isDeleted, const IdCollection& cells); void load(ESM::ESMReader& esm, bool& isDeleted); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/record.cpp000066400000000000000000000005211503074453300230610ustar00rootroot00000000000000#include "record.hpp" bool CSMWorld::RecordBase::isDeleted() const { return mState == State_Deleted || mState == State_Erased; } bool CSMWorld::RecordBase::isErased() const { return mState == State_Erased; } bool CSMWorld::RecordBase::isModified() const { return mState == State_Modified || mState == State_ModifiedOnly; }openmw-openmw-0.49.0/apps/opencs/model/world/record.hpp000066400000000000000000000107611503074453300230750ustar00rootroot00000000000000#ifndef CSM_WOLRD_RECORD_H #define CSM_WOLRD_RECORD_H #include #include namespace CSMWorld { struct RecordBase { enum State { State_BaseOnly = 0, // defined in base only State_Modified = 1, // exists in base, but has been modified State_ModifiedOnly = 2, // newly created in modified State_Deleted = 3, // exists in base, but has been deleted State_Erased = 4 // does not exist at all (we mostly treat that the same way as deleted) }; State mState; explicit RecordBase(State state) : mState(state) { } virtual ~RecordBase() = default; virtual std::unique_ptr clone() const = 0; virtual std::unique_ptr modifiedCopy() const = 0; virtual void assign(const RecordBase& record) = 0; ///< Will throw an exception if the types don't match. bool isDeleted() const; bool isErased() const; bool isModified() const; }; template struct Record : public RecordBase { ESXRecordT mBase; ESXRecordT mModified; Record(); Record(State state, const ESXRecordT* base = 0, const ESXRecordT* modified = 0); std::unique_ptr clone() const override; std::unique_ptr modifiedCopy() const override; void assign(const RecordBase& record) override; const ESXRecordT& get() const; ///< Throws an exception, if the record is deleted. ESXRecordT& get(); ///< Throws an exception, if the record is deleted. const ESXRecordT& getBase() const; ///< Throws an exception, if the record is deleted. Returns modified, if there is no base. void setModified(const ESXRecordT& modified); ///< Throws an exception, if the record is deleted. void merge(); ///< Merge modified into base. }; template Record::Record() : RecordBase(State_BaseOnly) , mBase() , mModified() { } template Record::Record(State state, const ESXRecordT* base, const ESXRecordT* modified) : RecordBase(state) , mBase(base == nullptr ? ESXRecordT{} : *base) , mModified(modified == nullptr ? ESXRecordT{} : *modified) { } template std::unique_ptr Record::modifiedCopy() const { return std::make_unique>(Record(State_ModifiedOnly, nullptr, &(this->get()))); } template std::unique_ptr Record::clone() const { return std::make_unique>(Record(*this)); } template void Record::assign(const RecordBase& 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"); 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"); 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"); return mState == State_ModifiedOnly ? mModified : mBase; } template void Record::setModified(const ESXRecordT& modified) { if (mState == State_Erased) throw std::logic_error("attempt to modify a deleted record"); mModified = modified; if (mState != State_ModifiedOnly) mState = State_Modified; } template void Record::merge() { if (isModified()) { mBase = mModified; mState = State_BaseOnly; } else if (mState == State_Deleted) { mState = State_Erased; } } } #endif openmw-openmw-0.49.0/apps/opencs/model/world/ref.cpp000066400000000000000000000004511503074453300223610ustar00rootroot00000000000000#include "ref.hpp" #include #include "cellcoordinates.hpp" CSMWorld::CellRef::CellRef() : mNew(true) , mIdNum(0) { } std::pair CSMWorld::CellRef::getCellIndex() const { return CellCoordinates::coordinatesToCellIndex(mPos.pos[0], mPos.pos[1]); } openmw-openmw-0.49.0/apps/opencs/model/world/ref.hpp000066400000000000000000000011111503074453300223600ustar00rootroot00000000000000#ifndef CSM_WOLRD_REF_H #define CSM_WOLRD_REF_H #include #include #include namespace CSMWorld { /// \brief Wrapper for CellRef sub record struct CellRef : public ESM::CellRef { 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(); /// Calculate cell index based on coordinates (x and y) std::pair getCellIndex() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/refcollection.cpp000066400000000000000000000300171503074453300244360ustar00rootroot00000000000000#include "refcollection.hpp" #include #include #include #include #include #include #include #include #include #include "cell.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() } 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); Cell& cell2 = base ? cell.mBase : cell.mModified; ESM::MovedCellRef mref; bool isDeleted = false; bool isMoved = false; 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 : ESM::RefId(); if (cell.get().isExterior()) { // Autocalculate the cell index from coordinates first std::pair index = ref.getCellIndex(); ref.mCell = ESM::RefId::stringRefId(ESM::RefId::esm3ExteriorCell(index.first, index.second).toString()); // Handle non-base moved references 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 ref.mOriginalCell = cell2.mId; // Some mods may move references outside of the bounds, which often happens they are deleted. // This results in nonsensical autocalculated cell IDs, so we must use the record target cell. // Log a warning if the record target cell is different if (index.first != mref.mTarget[0] || index.second != 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.toDebugString() + " (cell " + indexCell.toDebugString() + ")" " does not match the target cell (" + ref.mCell.toDebugString() + ")", std::string(), CSMDoc::Message::Severity_Warning); } } } else ref.mCell = cell2.mId; auto iter = cache.find(ref.mRefNum); if (isMoved) { if (iter == cache.end()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); messages.add(id, "Attempt to move a non-existent reference - RefNum index " + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID.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()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); 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 = getIntIndex(iter->second); if (base) { removeRows(index, 1); cache.erase(iter); } else { auto record = std::make_unique>(getRecord(index)); record->mState = RecordBase::State_Deleted; setRecord(index, std::move(record)); } continue; } if (iter == cache.end()) { // new reference ref.mIdNum = mNextId; // FIXME: fragile ref.mId = ESM::RefId::stringRefId(getNewId()); cache.emplace(ref.mRefNum, ref.mIdNum); auto record = std::make_unique>(); record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record->mBase : record->mModified) = std::move(ref); appendRecord(std::move(record)); } else { // old reference -> merge 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 // 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()); 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, std::move(record)); } } } 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)); } openmw-openmw-0.49.0/apps/opencs/model/world/refcollection.hpp000066400000000000000000000041341503074453300244440ustar00rootroot00000000000000#ifndef CSM_WOLRD_REFCOLLECTION_H #define CSM_WOLRD_REFCOLLECTION_H #include #include #include #include #include #include #include "collection.hpp" #include "record.hpp" #include "ref.hpp" namespace ESM { class ESMReader; } namespace CSMDoc { class Messages; } namespace CSMWorld { struct Cell; 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 final : public Collection { Collection& mCells; std::map mRefIndex; // CellRef index keyed by CSMWorld::CellRef::mIdNum int mNextId; unsigned int extractIdNum(std::string_view id) const; int getIntIndex(unsigned int id) const; int searchId(unsigned int id) const; public: // MSVC needs the constructor for a class inheriting a template to be defined in header RefCollection(Collection& cells) : 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; int searchId(const ESM::RefId& id) const override; void appendRecord(std::unique_ptr record, UniversalId::Type type = UniversalId::Type_None) override; void insertRecord( std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None) override; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/refidadapter.hpp000066400000000000000000000054671503074453300242600ustar00rootroot00000000000000#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). * * 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; namespace CSMWorld { class RefIdColumn; class RefIdData; struct RecordBase; struct NestedTableWrapperBase; class RefIdAdapter { public: RefIdAdapter() = default; RefIdAdapter(const RefIdAdapter&) = delete; RefIdAdapter& operator=(const RefIdAdapter&) = delete; virtual ~RefIdAdapter() = default; 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 ESM::RefId getId(const RecordBase& record) const = 0; virtual void setId(RecordBase& record, const std::string& id) = 0; // used by RefIdCollection::cloneRecord() }; class NestedRefIdAdapterBase { public: NestedRefIdAdapterBase() = default; virtual ~NestedRefIdAdapterBase() = default; 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 int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) 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 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 NestedTableWrapperBase* nestedTable( const RefIdColumn* column, const RefIdData& data, int index) const = 0; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/refidadapterimp.cpp000066400000000000000000001561571503074453300247640ustar00rootroot00000000000000#include "refidadapterimp.hpp" #include #include #include #include #include #include #include #include #include #include #include "nestedtablewrapper.hpp" 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) { } 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))); 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) return QVariant::fromValue(ColumnBase::TableEdit_Full); return InventoryRefIdAdapter::getData(column, data, index); } 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))); ESM::Potion potion = record.get(); if (column == mAutoCalc) potion.mData.mFlags = value.toBool(); else { InventoryRefIdAdapter::setData(column, data, index, value); return; } record.setModified(potion); } CSMWorld::IngredientColumns::IngredientColumns(const InventoryColumns& columns) : InventoryColumns(columns) , mEffects(nullptr) { } 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 { if (column == mColumns.mEffects) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); return InventoryRefIdAdapter::getData(column, data, index); } void CSMWorld::IngredientRefIdAdapter::setData( const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { InventoryRefIdAdapter::setData(column, data, index, value); return; } CSMWorld::IngredEffectRefIdAdapter::IngredEffectRefIdAdapter() : mType(UniversalId::Type_Ingredient) { } 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 { // 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESM::Ingredient ingredient = record.get(); ingredient.mData = static_cast>&>(nestedTable) .mNestedTable.at(0); record.setModified(ingredient); } 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))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } 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))); if (subRowIndex < 0 || subRowIndex >= 4) throw std::runtime_error("index out of range"); switch (subColIndex) { case 0: return record.get().mData.mEffectID[subRowIndex]; case 1: { switch (record.get().mData.mEffectID[subRowIndex]) { case ESM::MagicEffect::DrainSkill: case ESM::MagicEffect::DamageSkill: case ESM::MagicEffect::RestoreSkill: case ESM::MagicEffect::FortifySkill: case ESM::MagicEffect::AbsorbSkill: return record.get().mData.mSkills[subRowIndex]; default: return QVariant(); } } case 2: { switch (record.get().mData.mEffectID[subRowIndex]) { case ESM::MagicEffect::DrainAttribute: case ESM::MagicEffect::DamageAttribute: case ESM::MagicEffect::RestoreAttribute: case ESM::MagicEffect::FortifyAttribute: case ESM::MagicEffect::AbsorbAttribute: return record.get().mData.mAttributes[subRowIndex]; default: return QVariant(); } } default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } } 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))); ESM::Ingredient ingredient = record.get(); if (subRowIndex < 0 || subRowIndex >= 4) throw std::runtime_error("index out of range"); switch (subColIndex) { 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); } 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 { 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) { } 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))); if (column == mType) return record.get().mData.mType; if (column == mQuality) return record.get().mData.mQuality; return InventoryRefIdAdapter::getData(column, data, index); } 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))); ESM::Apparatus apparatus = record.get(); if (column == mType) apparatus.mData.mType = value.toInt(); else if (column == mQuality) apparatus.mData.mQuality = value.toFloat(); else { 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) { } 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))); if (column == mType) return record.get().mData.mType; if (column == mHealth) return record.get().mData.mHealth; if (column == mArmor) return record.get().mData.mArmor; if (column == mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); return EnchantableRefIdAdapter::getData(column, data, index); } 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))); ESM::Armor armor = record.get(); if (column == mType) armor.mData.mType = value.toInt(); else if (column == mHealth) armor.mData.mHealth = value.toInt(); else if (column == mArmor) armor.mData.mArmor = value.toInt(); else { EnchantableRefIdAdapter::setData(column, data, index, value); return; } 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) { } 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))); if (column == mBookType) return record.get().mData.mIsScroll; if (column == mSkill) return record.get().mData.mSkillId; if (column == mText) return QString::fromUtf8(record.get().mText.c_str()); return EnchantableRefIdAdapter::getData(column, data, index); } 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))); ESM::Book book = record.get(); if (column == mBookType) book.mData.mIsScroll = value.toInt(); else if (column == mSkill) book.mData.mSkillId = value.toInt(); else if (column == mText) book.mText = value.toString().toUtf8().data(); else { EnchantableRefIdAdapter::setData(column, data, index, value); return; } record.setModified(book); } 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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Clothing))); if (column == mType) return record.get().mData.mType; if (column == mPartRef) return QVariant::fromValue(ColumnBase::TableEdit_Full); return EnchantableRefIdAdapter::getData(column, data, index); } 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))); ESM::Clothing clothing = record.get(); if (column == mType) clothing.mData.mType = value.toInt(); else { EnchantableRefIdAdapter::setData(column, data, index, value); return; } 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) { } 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))); if (column == mWeight) return record.get().mWeight; if (column == mOrganic) return (record.get().mFlags & ESM::Container::Organic) != 0; if (column == mRespawn) return (record.get().mFlags & ESM::Container::Respawn) != 0; if (column == mContent) return QVariant::fromValue(ColumnBase::TableEdit_Full); return NameRefIdAdapter::getData(column, data, index); } 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))); ESM::Container container = record.get(); if (column == mWeight) container.mWeight = value.toFloat(); else if (column == mOrganic) { if (value.toInt()) container.mFlags |= ESM::Container::Organic; else container.mFlags &= ~ESM::Container::Organic; } else if (column == mRespawn) { if (value.toInt()) container.mFlags |= ESM::Container::Respawn; else container.mFlags &= ~ESM::Container::Respawn; } else { NameRefIdAdapter::setData(column, data, index, value); return; } 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::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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); if (column == mColumns.mType) return record.get().mData.mType; if (column == mColumns.mScale) return record.get().mScale; if (column == mColumns.mOriginal) return QString::fromUtf8(record.get().mOriginal.getRefIdString().c_str()); if (column == mColumns.mAttributes) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); if (column == mColumns.mAttacks) return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); 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); if (iter != mColumns.mFlags.end()) return (record.get().mFlags & iter->second) != 0; } { 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Creature))); ESM::Creature creature = record.get(); if (column == mColumns.mType) creature.mData.mType = value.toInt(); else if (column == mColumns.mScale) creature.mScale = value.toFloat(); 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); if (iter != mColumns.mFlags.end()) { if (value.toInt() != 0) creature.mFlags |= iter->second; else creature.mFlags &= ~iter->second; } else { ActorRefIdAdapter::setData(column, data, index, value); return; } } record.setModified(creature); } 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 { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Door))); if (column == mOpenSound) return QString::fromUtf8(record.get().mOpenSound.getRefIdString().c_str()); if (column == mCloseSound) return QString::fromUtf8(record.get().mCloseSound.getRefIdString().c_str()); return NameRefIdAdapter::getData(column, data, index); } 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))); ESM::Door door = record.get(); 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); return; } record.setModified(door); } 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) { } 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))); if (column == mColumns.mTime) return record.get().mData.mTime; if (column == mColumns.mRadius) return record.get().mData.mRadius; if (column == mColumns.mColor) return record.get().mData.mColor; if (column == mColumns.mSound) return QString::fromUtf8(record.get().mSound.getRefIdString().c_str()); if (column == mColumns.mEmitterType) { int mask = ESM::Light::Flicker | ESM::Light::FlickerSlow | ESM::Light::Pulse | ESM::Light::PulseSlow; if ((record.get().mData.mFlags & mask) == ESM::Light::Flicker) return 1; if ((record.get().mData.mFlags & mask) == ESM::Light::FlickerSlow) return 2; if ((record.get().mData.mFlags & mask) == ESM::Light::Pulse) return 3; if ((record.get().mData.mFlags & mask) == ESM::Light::PulseSlow) return 4; return 0; } std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) return (record.get().mData.mFlags & iter->second) != 0; return InventoryRefIdAdapter::getData(column, data, index); } 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))); ESM::Light light = record.get(); if (column == mColumns.mTime) light.mData.mTime = value.toInt(); else if (column == mColumns.mRadius) light.mData.mRadius = value.toInt(); else if (column == mColumns.mColor) light.mData.mColor = value.toInt(); 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); if (value.toInt() == 0) light.mData.mFlags = light.mData.mFlags & mask; else if (value.toInt() == 1) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::Flicker; else if (value.toInt() == 2) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::FlickerSlow; else if (value.toInt() == 3) light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::Pulse; else light.mData.mFlags = (light.mData.mFlags & mask) | ESM::Light::PulseSlow; } else { std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) { if (value.toInt() != 0) light.mData.mFlags |= iter->second; else light.mData.mFlags &= ~iter->second; } else { InventoryRefIdAdapter::setData(column, data, index, value); return; } } record.setModified(light); } 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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Miscellaneous))); if (column == mKey) return bool(record.get().mData.mFlags & ESM::Miscellaneous::Key); return InventoryRefIdAdapter::getData(column, data, index); } 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))); ESM::Miscellaneous misc = record.get(); if (column == mKey) misc.mData.mFlags = value.toInt(); else { InventoryRefIdAdapter::setData(column, data, index, value); return; } 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::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 { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Npc))); if (column == mColumns.mRace) return QString::fromUtf8(record.get().mRace.getRefIdString().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.getRefIdString().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.getRefIdString().c_str()); if (column == mColumns.mAttributes || column == mColumns.mSkills) { if ((record.get().mFlags & ESM::NPC::Autocalc) != 0) return QVariant::fromValue(ColumnBase::TableEdit_None); else return QVariant::fromValue(ColumnBase::TableEdit_FixedRows); } if (column == mColumns.mMisc) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mColumns.mBloodType) return record.get().mBloodType; if (column == mColumns.mGender) { // Implemented this way to allow additional gender types in the future. if ((record.get().mFlags & ESM::NPC::Female) == ESM::NPC::Female) return 1; return 0; } std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) return (record.get().mFlags & iter->second) != 0; return ActorRefIdAdapter::getData(column, data, index); } 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))); ESM::NPC npc = record.get(); 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) { // Implemented this way to allow additional gender types in the future. if (value.toInt() == 1) npc.mFlags = (npc.mFlags & ~ESM::NPC::Female) | ESM::NPC::Female; else npc.mFlags = npc.mFlags & ~ESM::NPC::Female; } else { std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) { 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; } else { ActorRefIdAdapter::setData(column, data, index, value); return; } } record.setModified(npc); } 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 { // 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 { 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); record.setModified(npc); } 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))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } 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 ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subColIndex == 0) return subRowIndex; 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 { 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 && subRowIndex >= 0 && subRowIndex < ESM::Attribute::Length) npcStruct.mAttributes[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? record.setModified(npc); } 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 { return ESM::Attribute::Length; } 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 { // 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 { 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); record.setModified(npc); } 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))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mNpdt); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } 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 ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt; if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) throw std::runtime_error("index out of range"); if (subColIndex == 0) return subRowIndex; else if (subColIndex == 1) return static_cast(npcStruct.mSkills[subRowIndex]); else 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 { 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"); if (subColIndex == 1) npcStruct.mSkills[subRowIndex] = static_cast(value.toInt()); else return; // throw an exception here? record.setModified(npc); } 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 { // There are 27 skills return ESM::Skill::Length; } 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"); } 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"); } void CSMWorld::NpcMiscRefIdAdapter::setNestedTable( const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } CSMWorld::NestedTableWrapperBase* CSMWorld::NpcMiscRefIdAdapter::nestedTable( const RefIdColumn* column, const RefIdData& data, int index) const { 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 { 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 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; 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 { 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) { 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) { 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); } int CSMWorld::NpcMiscRefIdAdapter::getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const { return 8; // Level, Health, Mana, Fatigue, Disposition, Reputation, Rank, Gold } int CSMWorld::NpcMiscRefIdAdapter::getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const { return 1; // fixed at size 1 } 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 { // 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 { 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); record.setModified(creature); } 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))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } 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 ESM::Creature& creature = record.get(); if (subColIndex == 0) return subRowIndex; 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, UniversalId::Type_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 { return 2; } int CSMWorld::CreatureAttributesRefIdAdapter::getNestedRowsCount( const RefIdColumn* column, const RefIdData& data, int index) const { return ESM::Attribute::Length; } 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 { // 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 { 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); record.setModified(creature); } 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))); // return the whole struct std::vector wrap; wrap.push_back(record.get().mData); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(wrap); } 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 ESM::Creature& creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) 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"); } 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))); ESM::Creature creature = record.get(); if (subRowIndex < 0 || subRowIndex > 2) 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); } 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 { // There are 3 attacks return 3; } 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"); } 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"); } void CSMWorld::CreatureMiscRefIdAdapter::setNestedTable( const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const { throw std::logic_error("table operation not supported"); } CSMWorld::NestedTableWrapperBase* CSMWorld::CreatureMiscRefIdAdapter::nestedTable( const RefIdColumn* column, const RefIdData& data, int index) const { 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 { 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? } } 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))); ESM::Creature creature = record.get(); 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? } record.setModified(creature); } 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 { 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::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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, UniversalId::Type_Weapon))); if (column == mColumns.mType) return record.get().mData.mType; if (column == mColumns.mHealth) return record.get().mData.mHealth; if (column == mColumns.mSpeed) return record.get().mData.mSpeed; if (column == mColumns.mReach) return record.get().mData.mReach; for (int i = 0; i < 2; ++i) { if (column == mColumns.mChop[i]) return record.get().mData.mChop[i]; if (column == mColumns.mSlash[i]) return record.get().mData.mSlash[i]; if (column == mColumns.mThrust[i]) return record.get().mData.mThrust[i]; } std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) return (record.get().mData.mFlags & iter->second) != 0; return EnchantableRefIdAdapter::getData(column, data, index); } 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))); ESM::Weapon weapon = record.get(); if (column == mColumns.mType) weapon.mData.mType = value.toInt(); else if (column == mColumns.mHealth) weapon.mData.mHealth = value.toInt(); else if (column == mColumns.mSpeed) weapon.mData.mSpeed = value.toFloat(); else if (column == mColumns.mReach) weapon.mData.mReach = value.toFloat(); else if (column == mColumns.mChop[0]) weapon.mData.mChop[0] = value.toInt(); else if (column == mColumns.mChop[1]) weapon.mData.mChop[1] = value.toInt(); else if (column == mColumns.mSlash[0]) weapon.mData.mSlash[0] = value.toInt(); else if (column == mColumns.mSlash[1]) weapon.mData.mSlash[1] = value.toInt(); else if (column == mColumns.mThrust[0]) weapon.mData.mThrust[0] = value.toInt(); else if (column == mColumns.mThrust[1]) weapon.mData.mThrust[1] = value.toInt(); else { std::map::const_iterator iter = mColumns.mFlags.find(column); if (iter != mColumns.mFlags.end()) { if (value.toInt() != 0) weapon.mData.mFlags |= iter->second; else weapon.mData.mFlags &= ~iter->second; } else { EnchantableRefIdAdapter::setData(column, data, index, value); return; // Don't overwrite changes made by base class } } record.setModified(weapon); } openmw-openmw-0.49.0/apps/opencs/model/world/refidadapterimp.hpp000066400000000000000000002751451503074453300247700ustar00rootroot00000000000000#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 "columnbase.hpp" #include "nestedtablewrapper.hpp" #include "record.hpp" #include "refidadapter.hpp" #include "refiddata.hpp" #include "universalid.hpp" namespace CSMWorld { class RefIdColumn; struct BaseColumns { 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 class BaseRefIdAdapter : public RefIdAdapter { UniversalId::Type mType; BaseColumns mBase; public: BaseRefIdAdapter(UniversalId::Type type, const BaseColumns& base); ESM::RefId getId(const RecordBase& record) const override; void setId(RecordBase& record, const std::string& id) 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. UniversalId::Type getType() const; }; template BaseRefIdAdapter::BaseRefIdAdapter(UniversalId::Type type, const BaseColumns& base) : mType(type) , mBase(base) { } template void BaseRefIdAdapter::setId(RecordBase& record, const std::string& id) { (dynamic_cast&>(record).get().mId) = ESM::RefId::stringRefId(id); } template ESM::RefId BaseRefIdAdapter::getId(const RecordBase& record) const { return dynamic_cast&>(record).get().mId; } template QVariant BaseRefIdAdapter::getData(const RefIdColumn* column, const RefIdData& data, int index) const { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); if (column == mBase.mId) return QString::fromStdString(record.get().mId.toString()); if (column == mBase.mModified) { if (record.mState == Record::State_Erased) return static_cast(Record::State_Deleted); return static_cast(record.mState); } 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); 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 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* mPersistence; ModelColumns(const BaseColumns& base) : BaseColumns(base) , mModel(nullptr) , mPersistence(nullptr) { } }; /// \brief Adapter for IDs with models (all but levelled lists) template class ModelRefIdAdapter : public BaseRefIdAdapter { ModelColumns mModel; public: ModelRefIdAdapter(UniversalId::Type type, const ModelColumns& columns); 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. }; 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 { 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.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 { Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); RecordT record2 = record.get(); 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); return; } record.setModified(record2); } struct NameColumns : public ModelColumns { const RefIdColumn* mName; const RefIdColumn* mScript; NameColumns(const ModelColumns& base) : ModelColumns(base) , mName(nullptr) , mScript(nullptr) { } }; /// \brief Adapter for IDs with names (all but levelled lists and statics) template class NameRefIdAdapter : public ModelRefIdAdapter { NameColumns mName; public: NameRefIdAdapter(UniversalId::Type type, const NameColumns& columns); 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. }; 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 { 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.mScript) return QString::fromUtf8(record.get().mScript.getRefIdString().c_str()); return ModelRefIdAdapter::getData(column, data, index); } 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()))); RecordT record2 = record.get(); if (column == mName.mName) record2.mName = 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); return; } record.setModified(record2); } struct InventoryColumns : public NameColumns { const RefIdColumn* mIcon; const RefIdColumn* mWeight; const RefIdColumn* mValue; InventoryColumns(const NameColumns& base) : NameColumns(base) , mIcon(nullptr) , mWeight(nullptr) , mValue(nullptr) { } }; /// \brief Adapter for IDs that can go into an inventory template class InventoryRefIdAdapter : public NameRefIdAdapter { InventoryColumns mInventory; public: InventoryRefIdAdapter(UniversalId::Type type, const InventoryColumns& columns); 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. }; 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 { 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.mWeight) return record.get().mData.mWeight; if (column == mInventory.mValue) return record.get().mData.mValue; return NameRefIdAdapter::getData(column, data, index); } 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()))); RecordT record2 = record.get(); if (column == mInventory.mIcon) record2.mIcon = value.toString().toUtf8().constData(); else if (column == mInventory.mWeight) record2.mData.mWeight = value.toFloat(); else if (column == mInventory.mValue) record2.mData.mValue = value.toInt(); else { NameRefIdAdapter::setData(column, data, index, value); return; } record.setModified(record2); } struct PotionColumns : public InventoryColumns { const RefIdColumn* mEffects; PotionColumns(const InventoryColumns& columns); }; class PotionRefIdAdapter : public InventoryRefIdAdapter { PotionColumns mColumns; const RefIdColumn* mAutoCalc; public: PotionRefIdAdapter(const PotionColumns& columns, const RefIdColumn* autoCalc); 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. }; struct IngredientColumns : public InventoryColumns { const RefIdColumn* mEffects; IngredientColumns(const InventoryColumns& columns); }; class IngredientRefIdAdapter : public InventoryRefIdAdapter { IngredientColumns mColumns; public: IngredientRefIdAdapter(const IngredientColumns& columns); 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. }; class IngredEffectRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; public: IngredEffectRefIdAdapter(); IngredEffectRefIdAdapter(const IngredEffectRefIdAdapter&) = delete; IngredEffectRefIdAdapter& operator=(const IngredEffectRefIdAdapter&) = delete; ~IngredEffectRefIdAdapter() override = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; struct EnchantableColumns : public InventoryColumns { const RefIdColumn* mEnchantment; const RefIdColumn* mEnchantmentPoints; EnchantableColumns(const InventoryColumns& base) : InventoryColumns(base) , mEnchantment(nullptr) , mEnchantmentPoints(nullptr) { } }; /// \brief Adapter for enchantable IDs template class EnchantableRefIdAdapter : public InventoryRefIdAdapter { EnchantableColumns mEnchantable; public: EnchantableRefIdAdapter(UniversalId::Type type, const EnchantableColumns& columns); 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. }; 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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); if (column == mEnchantable.mEnchantment) return QString::fromUtf8(record.get().mEnchant.getRefIdString().c_str()); if (column == mEnchantable.mEnchantmentPoints) return static_cast(record.get().mData.mEnchant); return InventoryRefIdAdapter::getData(column, data, index); } 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()))); RecordT record2 = record.get(); 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); return; } record.setModified(record2); } struct ToolColumns : public InventoryColumns { const RefIdColumn* mQuality; const RefIdColumn* mUses; ToolColumns(const InventoryColumns& base) : InventoryColumns(base) , mQuality(nullptr) , mUses(nullptr) { } }; /// \brief Adapter for tools with limited uses IDs (lockpick, repair, probes) template class ToolRefIdAdapter : public InventoryRefIdAdapter { ToolColumns mTools; public: ToolRefIdAdapter(UniversalId::Type type, const ToolColumns& columns); 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. }; 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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); if (column == mTools.mQuality) return record.get().mData.mQuality; if (column == mTools.mUses) return record.get().mData.mUses; return InventoryRefIdAdapter::getData(column, data, index); } 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()))); RecordT record2 = record.get(); if (column == mTools.mQuality) record2.mData.mQuality = value.toFloat(); else if (column == mTools.mUses) record2.mData.mUses = value.toInt(); else { InventoryRefIdAdapter::setData(column, data, index, value); return; } record.setModified(record2); } 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) , mHello(nullptr) , mFlee(nullptr) , mFight(nullptr) , mAlarm(nullptr) , mInventory(nullptr) , mSpells(nullptr) , mDestinations(nullptr) , mAiPackages(nullptr) { } }; /// \brief Adapter for actor IDs (handles common AI functionality) template class ActorRefIdAdapter : public NameRefIdAdapter { ActorColumns mActors; public: ActorRefIdAdapter(UniversalId::Type type, const ActorColumns& columns); 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. }; 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 { const Record& record = static_cast&>( data.getRecord(RefIdData::LocalIndex(index, BaseRefIdAdapter::getType()))); if (column == mActors.mHello) return record.get().mAiData.mHello; if (column == mActors.mFlee) return record.get().mAiData.mFlee; if (column == mActors.mFight) return record.get().mAiData.mFight; if (column == mActors.mAlarm) return record.get().mAiData.mAlarm; if (column == mActors.mInventory) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mActors.mSpells) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mActors.mDestinations) return QVariant::fromValue(ColumnBase::TableEdit_Full); if (column == mActors.mAiPackages) return QVariant::fromValue(ColumnBase::TableEdit_Full); std::map::const_iterator iter = mActors.mServices.find(column); if (iter != mActors.mServices.end()) return (record.get().mAiData.mServices & iter->second) != 0; return NameRefIdAdapter::getData(column, data, index); } 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()))); RecordT record2 = record.get(); if (column == mActors.mHello) record2.mAiData.mHello = value.toInt(); 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) record2.mAiData.mFight = std::min(100, value.toInt()); 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()) { if (value.toInt() != 0) record2.mAiData.mServices |= iter->second; else record2.mAiData.mServices &= ~iter->second; } else { NameRefIdAdapter::setData(column, data, index, value); return; } } record.setModified(record2); } class ApparatusRefIdAdapter : public InventoryRefIdAdapter { const RefIdColumn* mType; const RefIdColumn* mQuality; public: ApparatusRefIdAdapter(const InventoryColumns& columns, const RefIdColumn* type, const RefIdColumn* quality); 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. }; class ArmorRefIdAdapter : public EnchantableRefIdAdapter { 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); 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. }; class BookRefIdAdapter : public EnchantableRefIdAdapter { const RefIdColumn* mBookType; const RefIdColumn* mSkill; const RefIdColumn* mText; 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; 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; public: ClothingRefIdAdapter(const EnchantableColumns& columns, const RefIdColumn* type, const RefIdColumn* partRef); 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. }; class ContainerRefIdAdapter : public NameRefIdAdapter { const RefIdColumn* mWeight; const RefIdColumn* mOrganic; const RefIdColumn* mRespawn; const RefIdColumn* mContent; 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; 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; CreatureColumns(const ActorColumns& actorColumns); }; class CreatureRefIdAdapter : public ActorRefIdAdapter { CreatureColumns mColumns; public: CreatureRefIdAdapter(const CreatureColumns& columns); 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. }; class DoorRefIdAdapter : public NameRefIdAdapter { const RefIdColumn* mOpenSound; const RefIdColumn* mCloseSound; public: DoorRefIdAdapter(const NameColumns& columns, const RefIdColumn* openSound, const RefIdColumn* closeSound); 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. }; struct LightColumns : public InventoryColumns { const RefIdColumn* mTime; const RefIdColumn* mRadius; const RefIdColumn* mColor; const RefIdColumn* mSound; const RefIdColumn* mEmitterType; std::map mFlags; LightColumns(const InventoryColumns& columns); }; class LightRefIdAdapter : public InventoryRefIdAdapter { LightColumns mColumns; public: LightRefIdAdapter(const LightColumns& columns); 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. }; class MiscRefIdAdapter : public InventoryRefIdAdapter { const RefIdColumn* mKey; public: MiscRefIdAdapter(const InventoryColumns& columns, const RefIdColumn* key); 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. }; 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); }; class NpcRefIdAdapter : public ActorRefIdAdapter { NpcColumns mColumns; public: NpcRefIdAdapter(const NpcColumns& columns); 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. }; 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; WeaponColumns(const EnchantableColumns& columns); }; class WeaponRefIdAdapter : public EnchantableRefIdAdapter { WeaponColumns mColumns; public: WeaponRefIdAdapter(const WeaponColumns& columns); 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. }; class NpcAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: NpcAttributesRefIdAdapter() = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class NpcSkillsRefIdAdapter : public NestedRefIdAdapterBase { public: NpcSkillsRefIdAdapter() = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class NpcMiscRefIdAdapter : public NestedRefIdAdapterBase { public: NpcMiscRefIdAdapter() = default; NpcMiscRefIdAdapter(const NpcMiscRefIdAdapter&) = delete; NpcMiscRefIdAdapter& operator=(const NpcMiscRefIdAdapter&) = delete; ~NpcMiscRefIdAdapter() override = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureAttributesRefIdAdapter : public NestedRefIdAdapterBase { public: CreatureAttributesRefIdAdapter() = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureAttackRefIdAdapter : public NestedRefIdAdapterBase { public: CreatureAttackRefIdAdapter() = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; class CreatureMiscRefIdAdapter : public NestedRefIdAdapterBase { public: CreatureMiscRefIdAdapter() = default; CreatureMiscRefIdAdapter(const CreatureMiscRefIdAdapter&) = delete; CreatureMiscRefIdAdapter& operator=(const CreatureMiscRefIdAdapter&) = delete; ~CreatureMiscRefIdAdapter() override = default; 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 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; 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; int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override; int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override; }; template class EffectsListAdapter; template class EffectsRefIdAdapter : public EffectsListAdapter, public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented EffectsRefIdAdapter(const EffectsRefIdAdapter&); EffectsRefIdAdapter& operator=(const EffectsRefIdAdapter&); public: EffectsRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~EffectsRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { 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 { 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 { 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 { 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 { 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 { 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 { const Record record; // not used, just a dummy return EffectsListAdapter::getColumnsCount(record); } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return EffectsListAdapter::getRowsCount(record); } }; template class NestedInventoryRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedInventoryRefIdAdapter(const NestedInventoryRefIdAdapter&); NestedInventoryRefIdAdapter& operator=(const NestedInventoryRefIdAdapter&); public: NestedInventoryRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~NestedInventoryRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT container = record.get(); std::vector& list = container.mInventory.mList; ESM::ContItem newRow = ESM::ContItem(); if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin() + position, newRow); record.setModified(container); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { 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"); list.erase(list.begin() + rowToRemove); record.setModified(container); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT container = record.get(); container.mInventory.mList = static_cast>&>(nestedTable).mNestedTable; record.setModified(container); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mInventory.mList); } 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 std::vector& list = record.get().mInventory.mList; 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.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 { 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"); switch (subColIndex) { case 0: list.at(subRowIndex).mItem = ESM::RefId::stringRefId(value.toString().toUtf8().constData()); break; case 1: list.at(subRowIndex).mCount = value.toInt(); break; default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified(container); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 2; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mInventory.mList.size()); } }; template class NestedSpellRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedSpellRefIdAdapter(const NestedSpellRefIdAdapter&); NestedSpellRefIdAdapter& operator=(const NestedSpellRefIdAdapter&); public: NestedSpellRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~NestedSpellRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; ESM::RefId newString; if (position >= (int)list.size()) list.push_back(newString); else list.insert(list.begin() + position, newString); record.setModified(caster); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; if (rowToRemove < 0 || rowToRemove >= static_cast(list.size())) throw std::runtime_error("index out of range"); list.erase(list.begin() + rowToRemove); record.setModified(caster); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT caster = record.get(); caster.mSpells.mList = static_cast>&>(nestedTable).mNestedTable; record.setModified(caster); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mSpells.mList); } 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 std::vector& list = record.get().mSpells.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) throw std::runtime_error("index out of range"); const ESM::RefId& content = list.at(subRowIndex); if (subColIndex == 0) 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT caster = record.get(); std::vector& list = caster.mSpells.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) throw std::runtime_error("index out of range"); if (subColIndex == 0) 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); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 1; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mSpells.mList.size()); } }; template class NestedTravelRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedTravelRefIdAdapter(const NestedTravelRefIdAdapter&); NestedTravelRefIdAdapter& operator=(const NestedTravelRefIdAdapter&); public: NestedTravelRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~NestedTravelRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT traveller = record.get(); std::vector& list = traveller.mTransport.mList; ESM::Position newPos; for (unsigned i = 0; i < 3; ++i) { newPos.pos[i] = 0; newPos.rot[i] = 0; } ESM::Transport::Dest newRow; newRow.mPos = newPos; newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin() + position, newRow); record.setModified(traveller); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { 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"); list.erase(list.begin() + rowToRemove); record.setModified(traveller); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT traveller = record.get(); traveller.mTransport.mList = static_cast>&>(nestedTable) .mNestedTable; record.setModified(traveller); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mTransport.mList); } 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 std::vector& list = record.get().mTransport.mList; 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]; 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 { 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"); switch (subColIndex) { 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); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 7; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mTransport.mList.size()); } }; template class ActorAiRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented ActorAiRefIdAdapter(const ActorAiRefIdAdapter&); ActorAiRefIdAdapter& operator=(const ActorAiRefIdAdapter&); public: ActorAiRefIdAdapter(UniversalId::Type type) : mType(type) { } 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT actor = record.get(); std::vector& list = actor.mAiPackage.mList; ESM::AIPackage newRow; newRow.mType = ESM::AI_Wander; newRow.mWander.mDistance = 0; newRow.mWander.mDuration = 0; newRow.mWander.mTimeOfDay = 0; for (int i = 0; i < 8; ++i) newRow.mWander.mIdle[i] = 0; newRow.mWander.mShouldRepeat = 1; newRow.mCellName.clear(); if (position >= (int)list.size()) list.push_back(newRow); else list.insert(list.begin() + position, newRow); record.setModified(actor); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { 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"); list.erase(list.begin() + rowToRemove); record.setModified(actor); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT actor = record.get(); actor.mAiPackage.mList = static_cast>&>(nestedTable) .mNestedTable; record.setModified(actor); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mAiPackage.mList); } 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 std::vector& list = record.get().mAiPackage.mList; if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) throw std::runtime_error("index out of range"); const ESM::AIPackage& content = list.at(subRowIndex); switch (subColIndex) { case 0: // 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; default: return QVariant(); } case 1: // wander dist if (content.mType == ESM::AI_Wander) return content.mWander.mDistance; else return QVariant(); 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 if (content.mType == ESM::AI_Wander) return content.mWander.mTimeOfDay; // FIXME: not sure of the format else return QVariant(); case 4: // wander idle case 5: case 6: case 7: case 8: case 9: case 10: case 11: if (content.mType == ESM::AI_Wander) return static_cast(content.mWander.mIdle[subColIndex - 4]); else return QVariant(); 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 if (content.mType == ESM::AI_Activate) return QString(content.mActivate.mName.toString().c_str()); else return QVariant(); case 14: // target id if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return QString(content.mTarget.mId.toString().c_str()); else return QVariant(); case 15: // target cell if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return QString::fromUtf8(content.mCellName.c_str()); else return QVariant(); case 16: if (content.mType == ESM::AI_Travel) return content.mTravel.mX; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mX; else return QVariant(); case 17: if (content.mType == ESM::AI_Travel) return content.mTravel.mY; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mY; else return QVariant(); case 18: if (content.mType == ESM::AI_Travel) return content.mTravel.mZ; else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) return content.mTarget.mZ; else return QVariant(); 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 { 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"); ESM::AIPackage& content = list.at(subRowIndex); 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 } break; // always save case 1: if (content.mType == ESM::AI_Wander) content.mWander.mDistance = static_cast(value.toInt()); else return; // return without saving break; // always save case 2: 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()); else return; // return without saving break; // always save case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: if (content.mType == ESM::AI_Wander) content.mWander.mIdle[subColIndex - 4] = static_cast(value.toInt()); else return; // return without saving break; // always save 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) { 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) { const QByteArray id = value.toString().toUtf8(); content.mTarget.mId.assign(std::string_view(id.constData(), id.size())); } else return; // return without saving break; // always save case 15: if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mCellName = std::string(value.toString().toUtf8().constData()); else return; // return without saving break; // always save case 16: if (content.mType == ESM::AI_Travel) content.mTravel.mX = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mX = value.toFloat(); else return; // return without saving break; // always save case 17: if (content.mType == ESM::AI_Travel) content.mTravel.mY = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mY = value.toFloat(); else return; // return without saving break; // always save case 18: if (content.mType == ESM::AI_Travel) content.mTravel.mZ = value.toFloat(); else if (content.mType == ESM::AI_Follow || content.mType == ESM::AI_Escort) content.mTarget.mZ = value.toFloat(); else return; // return without saving break; // always save default: throw std::runtime_error("Trying to access non-existing column in the nested table!"); } record.setModified(actor); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 19; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { 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&); public: BodyPartRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~BodyPartRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { 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 = ESM::RefId(); newPart.mFemale = ESM::RefId(); if (position >= (int)list.size()) list.push_back(newPart); else list.insert(list.begin() + position, newPart); record.setModified(apparel); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { 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"); list.erase(list.begin() + rowToRemove); record.setModified(apparel); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT apparel = record.get(); apparel.mParts.mParts = static_cast>&>(nestedTable) .mNestedTable; record.setModified(apparel); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mParts.mParts); } 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 std::vector& list = record.get().mParts.mParts; if (subRowIndex < 0 || subRowIndex >= static_cast(list.size())) throw std::runtime_error("index out of range"); const ESM::PartReference& content = list.at(subRowIndex); switch (subColIndex) { case 0: { if (content.mPart < ESM::PRT_Count) return content.mPart; else throw std::runtime_error("Part Reference Type unexpected value"); } 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 { 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"); switch (subColIndex) { 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); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 3; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { 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; LevListColumns(const BaseColumns& base) : BaseColumns(base) , mLevList(nullptr) , mNestedListLevList(nullptr) { } }; template class LevelledListRefIdAdapter : public BaseRefIdAdapter { LevListColumns mLevList; public: LevelledListRefIdAdapter(UniversalId::Type type, const LevListColumns& columns); 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. }; 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 { if (column == mLevList.mLevList || column == mLevList.mNestedListLevList) return QVariant::fromValue(ColumnBase::TableEdit_Full); return BaseRefIdAdapter::getData(column, data, index); } template void LevelledListRefIdAdapter::setData( const RefIdColumn* column, RefIdData& data, int index, const QVariant& value) const { BaseRefIdAdapter::setData(column, data, index, value); return; } // for non-tables template class NestedListLevListRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedListLevListRefIdAdapter(const NestedListLevListRefIdAdapter&); NestedListLevListRefIdAdapter& operator=(const NestedListLevListRefIdAdapter&); public: NestedListLevListRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~NestedListLevListRefIdAdapter() = default; 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"); } 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"); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { throw std::logic_error("table operation not supported"); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { throw std::logic_error("table operation not supported"); } 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))); 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); default: throw std::runtime_error("Trying to access non-existing column in levelled creatues!"); } } else { 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); 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 { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(row, mType))); ESXRecordT leveled = record.get(); if (mType == UniversalId::Type_CreatureLevelledList) { switch (subColIndex) { case 0: return; // return without saving case 1: { if (value.toBool()) { leveled.mFlags |= ESM::CreatureLevList::AllLevels; break; } else { leveled.mFlags &= ~ESM::CreatureLevList::AllLevels; 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) { case 0: { if (value.toBool()) { leveled.mFlags |= ESM::ItemLevList::Each; break; } else { leveled.mFlags &= ~ESM::ItemLevList::Each; break; } } case 1: { if (value.toBool()) { leveled.mFlags |= ESM::ItemLevList::AllLevels; break; } else { leveled.mFlags &= ~ESM::ItemLevList::AllLevels; 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); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 3; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { return 1; // fixed at size 1 } }; // for tables template class NestedLevListRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; // not implemented NestedLevListRefIdAdapter(const NestedLevListRefIdAdapter&); NestedLevListRefIdAdapter& operator=(const NestedLevListRefIdAdapter&); public: NestedLevListRefIdAdapter(UniversalId::Type type) : mType(type) { } virtual ~NestedLevListRefIdAdapter() = default; void addNestedRow(const RefIdColumn* column, RefIdData& data, int index, int position) const override { 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 = ESM::RefId(); newItem.mLevel = 0; if (position >= (int)list.size()) list.push_back(newItem); else list.insert(list.begin() + position, newItem); record.setModified(leveled); } void removeNestedRow(const RefIdColumn* column, RefIdData& data, int index, int rowToRemove) const override { 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"); list.erase(list.begin() + rowToRemove); record.setModified(leveled); } void setNestedTable(const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const override { Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); ESXRecordT leveled = record.get(); leveled.mList = static_cast>&>( nestedTable) .mNestedTable; record.setModified(leveled); } NestedTableWrapperBase* nestedTable(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); // deleted by dtor of NestedTableStoring return new NestedTableWrapper>(record.get().mList); } 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 std::vector& list = record.get().mList; 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.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 { 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"); switch (subColIndex) { 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); } int getNestedColumnsCount(const RefIdColumn* column, const RefIdData& data) const override { return 2; } int getNestedRowsCount(const RefIdColumn* column, const RefIdData& data, int index) const override { const Record& record = static_cast&>(data.getRecord(RefIdData::LocalIndex(index, mType))); return static_cast(record.get().mList.size()); } }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/refidcollection.cpp000066400000000000000000001267371503074453300247720ustar00rootroot00000000000000#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 "columns.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) { } bool CSMWorld::RefIdColumn::isEditable() const { return mEditable; } bool CSMWorld::RefIdColumn::isUserEditable() const { return mUserEditable; } const CSMWorld::RefIdAdapter& CSMWorld::RefIdCollection::findAdapter(UniversalId::Type type) const { std::map::const_iterator iter = mAdapters.find(type); if (iter == mAdapters.end()) throw std::logic_error("unsupported type in RefIdCollection"); return *iter->second; } CSMWorld::RefIdCollection::RefIdCollection() { BaseColumns baseColumns; 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); baseColumns.mModified = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_RecordType, ColumnBase::Display_RefRecordType, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue, false, false); baseColumns.mType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Blocked, ColumnBase::Display_Boolean, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh); baseColumns.mBlocked = &mColumns.back(); ModelColumns modelColumns(baseColumns); mColumns.emplace_back(Columns::ColumnId_Persistent, ColumnBase::Display_Boolean); modelColumns.mPersistence = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Model, ColumnBase::Display_Mesh); modelColumns.mModel = &mColumns.back(); NameColumns nameColumns(modelColumns); // 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); 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_GoldValue, ColumnBase::Display_Integer); inventoryColumns.mValue = &mColumns.back(); 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())); 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)); // nested table 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))); 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_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); 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); 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); mColumns.emplace_back(Columns::ColumnId_AiHello, ColumnBase::Display_UnsignedInteger16); actorsColumns.mHello = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiFlee, ColumnBase::Display_UnsignedInteger8); actorsColumns.mFlee = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiFight, ColumnBase::Display_UnsignedInteger8); actorsColumns.mFight = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_AiAlarm, ColumnBase::Display_UnsignedInteger8); actorsColumns.mAlarm = &mColumns.back(); // Nested table 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))); 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)); // Nested table 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))); mNestedAdapters.emplace_back(&mColumns.back(), spellsMap); 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); 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))); 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)); // Nested table 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))); 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_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) { mColumns.emplace_back(sServiceTable[i].mName, ColumnBase::Display_Boolean); 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(); 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(); mColumns.emplace_back(Columns::ColumnId_Health, ColumnBase::Display_Integer); const RefIdColumn* health = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ArmorValue, ColumnBase::Display_Integer); const RefIdColumn* armor = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_BookType, ColumnBase::Display_BookType); const RefIdColumn* bookType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Skill, ColumnBase::Display_SkillId); const RefIdColumn* skill = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Text, ColumnBase::Display_LongString); const RefIdColumn* text = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ClothingType, ColumnBase::Display_ClothingType); const RefIdColumn* clothingType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_WeightCapacity, ColumnBase::Display_Float); const RefIdColumn* weightCapacity = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_OrganicContainer, ColumnBase::Display_Boolean); const RefIdColumn* organic = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Respawn, ColumnBase::Display_Boolean); const RefIdColumn* respawn = &mColumns.back(); // Nested table 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))); 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)); CreatureColumns creatureColumns(actorsColumns); mColumns.emplace_back(Columns::ColumnId_CreatureType, ColumnBase::Display_CreatureType); creatureColumns.mType = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Scale, ColumnBase::Display_Float); creatureColumns.mScale = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_ParentCreature, ColumnBase::Display_Creature); creatureColumns.mOriginal = &mColumns.back(); static const struct { 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 } }; // for re-use in NPC records const RefIdColumn* essential = nullptr; 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)); switch (sCreatureFlagTable[i].mFlag) { 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(); creatureColumns.mBloodType = bloodType; creatureColumns.mFlags.insert(std::make_pair(respawn, ESM::Creature::Respawn)); // Nested table 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)); // Nested table 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)); // Nested 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.emplace_back(Columns::ColumnId_OpenSound, ColumnBase::Display_Sound); const RefIdColumn* openSound = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_CloseSound, ColumnBase::Display_Sound); const RefIdColumn* closeSound = &mColumns.back(); LightColumns lightColumns(inventoryColumns); mColumns.emplace_back(Columns::ColumnId_Duration, ColumnBase::Display_Integer); lightColumns.mTime = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Radius, ColumnBase::Display_Integer); lightColumns.mRadius = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Colour, ColumnBase::Display_Colour); lightColumns.mColor = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Sound, ColumnBase::Display_Sound); lightColumns.mSound = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_EmitterType, ColumnBase::Display_EmitterType); lightColumns.mEmitterType = &mColumns.back(); static const struct { 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) { mColumns.emplace_back(sLightFlagTable[i].mName, ColumnBase::Display_Boolean); 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(); NpcColumns npcColumns(actorsColumns); mColumns.emplace_back(Columns::ColumnId_Race, ColumnBase::Display_Race); npcColumns.mRace = &mColumns.back(); 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(); mColumns.emplace_back(Columns::Columnid_Hair, ColumnBase::Display_BodyPart); npcColumns.mHair = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_Head, ColumnBase::Display_BodyPart); npcColumns.mHead = &mColumns.back(); 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(respawn, ESM::NPC::Respawn)); npcColumns.mFlags.insert(std::make_pair(autoCalc, ESM::NPC::Autocalc)); // Re-used from Creature records. npcColumns.mBloodType = bloodType; // Need a way to add a table of stats and values (rather than adding a long list of // entries in the dialogue subview) E.g. attributes+stats(health, mana, fatigue), skills // These needs to be driven from the autocalculated setting. // Nested table 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)); // Nested table 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)); // Nested 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_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)); mColumns.back().addColumn( new RefIdColumn(Columns::ColumnId_NpcDisposition, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn(Columns::ColumnId_NpcReputation, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn( new RefIdColumn(Columns::ColumnId_NpcRank, CSMWorld::ColumnBase::Display_UnsignedInteger8)); mColumns.back().addColumn(new RefIdColumn(Columns::ColumnId_Gold, CSMWorld::ColumnBase::Display_Integer)); WeaponColumns weaponColumns(enchantableColumns); mColumns.emplace_back(Columns::ColumnId_WeaponType, ColumnBase::Display_WeaponType); weaponColumns.mType = &mColumns.back(); weaponColumns.mHealth = health; mColumns.emplace_back(Columns::ColumnId_WeaponSpeed, ColumnBase::Display_Float); weaponColumns.mSpeed = &mColumns.back(); mColumns.emplace_back(Columns::ColumnId_WeaponReach, ColumnBase::Display_Float); weaponColumns.mReach = &mColumns.back(); for (int i = 0; i < 3; ++i) { const RefIdColumn** column = i == 0 ? weaponColumns.mChop : (i == 1 ? weaponColumns.mSlash : weaponColumns.mThrust); for (int j = 0; j < 2; ++j) { mColumns.emplace_back(Columns::ColumnId_MinChop + i * 2 + j, ColumnBase::Display_Integer); column[j] = &mColumns.back(); } } static const struct { int mName; unsigned int mFlag; } sWeaponFlagTable[] = { { Columns::ColumnId_Magical, ESM::Weapon::Magical }, { Columns::ColumnId_Silver, ESM::Weapon::Silver }, { -1, 0 } }; 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)); } // Nested table mColumns.emplace_back(Columns::ColumnId_PartRefList, ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue); 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))); mNestedAdapters.emplace_back(&mColumns.back(), partMap); mColumns.back().addColumn( 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_LevelledCreatureId, CSMWorld::ColumnBase::Display_Referenceable)); mColumns.back().addColumn( new RefIdColumn(Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); // 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)); mColumns.back().addColumn( new RefIdColumn(Columns::ColumnId_LevelledItemLevel, CSMWorld::ColumnBase::Display_Integer)); // 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))); nestedListLevListMap.insert(std::make_pair(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, 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::vector>>::iterator iter( mNestedAdapters.begin()); iter != mNestedAdapters.end(); ++iter) { for (std::map::iterator it((iter->second).begin()); it != (iter->second).end(); ++it) delete it->second; } } int CSMWorld::RefIdCollection::getSize() const { return mData.getSize(); } ESM::RefId CSMWorld::RefIdCollection::getId(int index) const { return ESM::RefId::stringRefId(getData(index, 0).toString().toUtf8().constData()); } int CSMWorld::RefIdCollection::getIndex(const ESM::RefId& id) const { int index = searchId(id); if (index == -1) throw std::runtime_error("ID is not found in RefId collection: " + id.toDebugString()); return index; } int CSMWorld::RefIdCollection::getColumns() const { return mColumns.size(); } const CSMWorld::ColumnBase& CSMWorld::RefIdCollection::getColumn(int column) const { return mColumns.at(column); } QVariant CSMWorld::RefIdCollection::getData(int index, int column) const { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(index); const RefIdAdapter& adaptor = findAdapter(localIndex.second); return adaptor.getData(&mColumns.at(column), mData, localIndex.first); } 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); } void CSMWorld::RefIdCollection::setData(int index, int column, const QVariant& data) { RefIdData::LocalIndex localIndex = mData.globalToLocalIndex(index); const RefIdAdapter& adaptor = findAdapter(localIndex.second); 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); const CSMWorld::NestedRefIdAdapterBase& nestedAdapter = getNestedAdapter(mColumns.at(column), localIndex.second); nestedAdapter.setNestedData(&mColumns.at(column), mData, localIndex.first, data, subRow, subColumn); } void CSMWorld::RefIdCollection::removeRows(int index, int count) { mData.erase(index, count); } void CSMWorld::RefIdCollection::removeNestedRows(int row, int column, int subRow) { 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); } void CSMWorld::RefIdCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) { mData.appendRecord(type, id, false); } int CSMWorld::RefIdCollection::searchId(const ESM::RefId& id) const { const RefIdData::LocalIndex localIndex = mData.searchId(id); if (localIndex.first == -1) return -1; return mData.localToGlobalIndex(localIndex); } void CSMWorld::RefIdCollection::replace(int index, std::unique_ptr record) { mData.getRecord(mData.globalToLocalIndex(index)).assign(*record.release()); } 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(); 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 ESM::RefId& id) { throw std::runtime_error("RefIdCollection::touchRecord is unimplemented"); return false; } void CSMWorld::RefIdCollection::appendRecord(std::unique_ptr record, UniversalId::Type type) { auto id = findAdapter(type).getId(*record.get()); int index = mData.getAppendIndex(type); mData.appendRecord(type, id, false); mData.getRecord(mData.globalToLocalIndex(index)).assign(*record.release()); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord(const ESM::RefId& id) const { return mData.getRecord(mData.searchId(id)); } const CSMWorld::RecordBase& CSMWorld::RefIdCollection::getRecord(int index) const { return mData.getRecord(mData.globalToLocalIndex(index)); } void CSMWorld::RefIdCollection::load(ESM::ESMReader& reader, bool base, UniversalId::Type type) { mData.load(reader, base, type); } int CSMWorld::RefIdCollection::getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const { return mData.getAppendIndex(type); } std::vector CSMWorld::RefIdCollection::getIds(bool listDeleted) const { return mData.getIds(listDeleted); } bool CSMWorld::RefIdCollection::reorderRows(int baseIndex, const std::vector& newOrder) { return false; } void CSMWorld::RefIdCollection::save(int index, ESM::ESMWriter& writer) const { mData.save(index, writer); } const CSMWorld::RefIdData& CSMWorld::RefIdCollection::getDataSet() const { return mData; } int CSMWorld::RefIdCollection::getNestedRowsCount(int row, int column) const { 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); } int CSMWorld::RefIdCollection::getNestedColumnsCount(int row, int column) const { 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) { return &mColumns.at(column); } void CSMWorld::RefIdCollection::addNestedRow(int row, int col, int position) { 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); } void CSMWorld::RefIdCollection::setNestedTable(int row, int column, const CSMWorld::NestedTableWrapperBase& nestedTable) { 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); } CSMWorld::NestedTableWrapperBase* CSMWorld::RefIdCollection::nestedTable(int row, int column) const { 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 { 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); if (it == (iter->second).end()) throw std::runtime_error("No such type in the nestedadapters"); return *it->second; } } throw std::runtime_error("No such column in the nestedadapters"); } void CSMWorld::RefIdCollection::copyTo(int index, RefIdCollection& target) const { mData.copyTo(index, target.mData); } openmw-openmw-0.49.0/apps/opencs/model/world/refidcollection.hpp000066400000000000000000000116651503074453300247700ustar00rootroot00000000000000#ifndef CSM_WOLRD_REFIDCOLLECTION_H #define CSM_WOLRD_REFIDCOLLECTION_H #include #include #include #include #include #include #include #include #include #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: RefIdColumn(int columnId, Display displayType, int flag = Flag_Table | Flag_Dialogue, bool editable = true, bool userEditable = true); bool isEditable() 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: const RefIdAdapter& findAdapter(UniversalId::Type) const; ///< Throws an exception if no adaptor for \a Type can be found. const NestedRefIdAdapterBase& getNestedAdapter(const ColumnBase& column, UniversalId::Type type) const; public: RefIdCollection(); ~RefIdCollection() override; int getSize() const override; ESM::RefId getId(int index) const override; int getIndex(const ESM::RefId& id) const override; int getColumns() const override; const ColumnBase& getColumn(int column) const override; QVariant getData(int index, int column) const override; void setData(int index, int column, const QVariant& data) override; void removeRows(int index, int count) override; void cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) override; bool touchRecord(const ESM::RefId& id) override; 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 ESM::RefId& id) const override; ////< Search record with \a id. /// \return index of record (if found) or -1 (not found) 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(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 ESM::RefId& id) const override; const RecordBase& getRecord(int index) const override; void load(ESM::ESMReader& reader, bool base, UniversalId::Type type); 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 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; NestedTableWrapperBase* nestedTable(int row, int column) const override; void setNestedTable(int row, int column, const NestedTableWrapperBase& nestedTable) override; int getNestedRowsCount(int row, int column) const override; int getNestedColumnsCount(int row, int column) const override; NestableColumn* getNestableColumn(int column) 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 addNestedRow(int row, int col, int position) override; 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; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/refiddata.cpp000066400000000000000000000343111503074453300235320ustar00rootroot00000000000000#include "refiddata.hpp" #include #include #include #include #include #include namespace ESM { class ESMWriter; } ESM::RefId CSMWorld::RefIdData::getRecordId(const CSMWorld::RefIdData::LocalIndex& index) const { std::map::const_iterator found = mRecordContainers.find(index.second); if (found == mRecordContainers.end()) 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) { if (index < iter->second->getSize()) return LocalIndex(index, iter->first); index -= iter->second->getSize(); } throw std::runtime_error("RefIdData index out of range"); } int CSMWorld::RefIdData::localToGlobalIndex(const LocalIndex& index) const { std::map::const_iterator end = mRecordContainers.find(index.second); 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) globalIndex += iter->second->getSize(); return globalIndex; } CSMWorld::RefIdData::LocalIndex CSMWorld::RefIdData::searchId(const ESM::RefId& id) const { auto iter = mIndex.find(id); if (iter == mIndex.end()) return std::make_pair(-1, CSMWorld::UniversalId::Type_None); return iter->second; } 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); std::map::const_iterator iter = mRecordContainers.find(localIndex.second); while (count > 0 && iter != mRecordContainers.end()) { int size = iter->second->getSize(); if (localIndex.first + count > size) { erase(localIndex, size - localIndex.first); count -= size - localIndex.first; ++iter; 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); count = 0; } } } const CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord(const LocalIndex& index) const { std::map::const_iterator iter = mRecordContainers.find(index.second); if (iter == mRecordContainers.end()) throw std::logic_error("invalid local index type"); return iter->second->getRecord(index.first); } CSMWorld::RecordBase& CSMWorld::RefIdData::getRecord(const LocalIndex& index) { std::map::iterator iter = mRecordContainers.find(index.second); if (iter == mRecordContainers.end()) throw std::logic_error("invalid local index type"); return iter->second->getRecord(index.first); } void CSMWorld::RefIdData::appendRecord(UniversalId::Type type, const ESM::RefId& id, bool base) { std::map::iterator iter = mRecordContainers.find(type); if (iter == mRecordContainers.end()) throw std::logic_error("invalid local index type"); iter->second->appendRecord(id, base); mIndex.insert(std::make_pair(id, LocalIndex(iter->second->getSize() - 1, type))); } int CSMWorld::RefIdData::getAppendIndex(UniversalId::Type type) const { int index = 0; for (std::map::const_iterator iter(mRecordContainers.begin()); iter != mRecordContainers.end(); ++iter) { index += iter->second->getSize(); if (type == iter->first) break; } return index; } void CSMWorld::RefIdData::load(ESM::ESMReader& reader, bool base, CSMWorld::UniversalId::Type type) { std::map::iterator found = mRecordContainers.find(type); if (found == mRecordContainers.end()) throw std::logic_error("Invalid Referenceable ID type"); int index = found->second->load(reader, base); if (index != -1) { LocalIndex localIndex = LocalIndex(index, type); if (base && getRecord(localIndex).mState == RecordBase::State_Deleted) { erase(localIndex, 1); } else { mIndex[getRecordId(localIndex)] = localIndex; } } } 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"); for (int i = index.first; i < index.first + count; ++i) { auto result = mIndex.find(iter->second->getId(i)); if (result != mIndex.end()) mIndex.erase(result); } // Adjust the local indexes to avoid gaps between them after removal of records int recordIndex = index.first + count; int recordCount = iter->second->getSize(); while (recordIndex < recordCount) { auto recordIndexFound = mIndex.find(iter->second->getId(recordIndex)); if (recordIndexFound != mIndex.end()) { recordIndexFound->second.first -= count; } ++recordIndex; } iter->second->erase(index.first, count); } int CSMWorld::RefIdData::getSize() const { return mIndex.size(); } std::vector CSMWorld::RefIdData::getIds(bool listDeleted) const { std::vector ids; for (auto iter(mIndex.begin()); iter != mIndex.end(); ++iter) { if (listDeleted || !getRecord(iter->second).isDeleted()) { std::map::const_iterator container = mRecordContainers.find(iter->second.second); if (container == mRecordContainers.end()) throw std::logic_error("Invalid referenceable ID type"); ids.push_back(container->second->getId(iter->second.first)); } } return ids; } void CSMWorld::RefIdData::save(int index, ESM::ESMWriter& writer) const { LocalIndex localIndex = globalToLocalIndex(index); std::map::const_iterator iter = mRecordContainers.find(localIndex.second); if (iter == mRecordContainers.end()) throw std::logic_error("invalid local index type"); iter->second->save(localIndex.first, writer); } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getBooks() const { return mBooks; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getActivators() const { return mActivators; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getPotions() const { return mPotions; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getApparati() const { return mApparati; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getArmors() const { return mArmors; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getClothing() const { return mClothing; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getContainers() const { return mContainers; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getCreatures() const { return mCreatures; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getDoors() const { return mDoors; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getIngredients() const { return mIngredients; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getCreatureLevelledLists() const { return mCreatureLevelledLists; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getItemLevelledList() const { return mItemLevelledLists; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getLights() const { return mLights; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getLocpicks() const { return mLockpicks; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getMiscellaneous() const { return mMiscellaneous; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getNPCs() const { return mNpcs; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getWeapons() const { return mWeapons; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getProbes() const { return mProbes; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getRepairs() const { return mRepairs; } const CSMWorld::RefIdDataContainer& CSMWorld::RefIdData::getStatics() const { return mStatics; } void CSMWorld::RefIdData::insertRecord( std::unique_ptr record, CSMWorld::UniversalId::Type type, const ESM::RefId& id) { std::map::iterator iter = mRecordContainers.find(type); if (iter == mRecordContainers.end()) throw std::logic_error("invalid local index type"); iter->second->insertRecord(std::move(record)); mIndex.insert(std::make_pair(id, LocalIndex(iter->second->getSize() - 1, type))); } void CSMWorld::RefIdData::copyTo(int index, RefIdData& target) const { LocalIndex localIndex = globalToLocalIndex(index); auto foundIndex = mRecordContainers.find(localIndex.second); assert(foundIndex != mRecordContainers.end()); RefIdDataContainerBase* source = foundIndex->second; target.insertRecord( source->getRecord(localIndex.first).modifiedCopy(), localIndex.second, source->getId(localIndex.first)); } openmw-openmw-0.49.0/apps/opencs/model/world/refiddata.hpp000066400000000000000000000255761503074453300235540ustar00rootroot00000000000000#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 "record.hpp" #include "universalid.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct RefIdDataContainerBase { virtual ~RefIdDataContainerBase() = default; virtual int getSize() const = 0; virtual const RecordBase& getRecord(int index) const = 0; virtual RecordBase& getRecord(int index) = 0; virtual unsigned int getRecordFlags(int index) const = 0; virtual void appendRecord(const ESM::RefId& id, bool base) = 0; virtual void insertRecord(std::unique_ptr record) = 0; virtual int load(ESM::ESMReader& reader, bool base) = 0; ///< \return index of a loaded record or -1 if no record was loaded virtual void erase(int index, int count) = 0; virtual ESM::RefId getId(int index) const = 0; virtual void save(int index, ESM::ESMWriter& writer) const = 0; }; template struct RefIdDataContainer : public RefIdDataContainerBase { std::vector>> mContainer; int getSize() const override; const RecordBase& getRecord(int index) const override; RecordBase& getRecord(int index) override; unsigned int getRecordFlags(int index) const override; void appendRecord(const ESM::RefId& id, 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; ESM::RefId getId(int index) const override; void save(int index, ESM::ESMWriter& writer) const override; }; template void RefIdDataContainer::insertRecord(std::unique_ptr record) { 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 int RefIdDataContainer::getSize() const { return static_cast(mContainer.size()); } template const RecordBase& RefIdDataContainer::getRecord(int index) const { return *mContainer.at(index); } template RecordBase& RefIdDataContainer::getRecord(int index) { return *mContainer.at(index); } template unsigned int RefIdDataContainer::getRecordFlags(int index) const { return mContainer.at(index)->get().mRecordFlags; } template void RefIdDataContainer::appendRecord(const ESM::RefId& id, bool base) { auto record = std::make_unique>(); record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; record->mBase.mId = id; record->mModified.mId = id; (base ? record->mBase : record->mModified).blank(); mContainer.push_back(std::move(record)); } template int RefIdDataContainer::load(ESM::ESMReader& reader, bool base) { RecordT record; bool isDeleted = false; record.load(reader, isDeleted); int index = 0; int numRecords = static_cast(mContainer.size()); for (; index < numRecords; ++index) { if ((mContainer[index]->get().mId == record.mId)) { break; } } if (isDeleted) { if (index == numRecords) { // deleting a record that does not exist // ignore it for now /// \todo report the problem to the user return -1; } // Flag the record as Deleted even for a base content file. // RefIdData is responsible for its erasure. mContainer[index]->mState = RecordBase::State_Deleted; } else { if (index == numRecords) { appendRecord(record.mId, base); if (base) { mContainer.back()->mBase = record; } else { mContainer.back()->mModified = record; } } else if (!base) { mContainer[index]->setModified(record); } else { // Overwrite mContainer[index]->setModified(record); mContainer[index]->merge(); } } return index; } template void RefIdDataContainer::erase(int index, int count) { if (index < 0 || index + count > getSize()) throw std::runtime_error("invalid RefIdDataContainer index"); mContainer.erase(mContainer.begin() + index, mContainer.begin() + index + count); } template ESM::RefId RefIdDataContainer::getId(int index) const { return mContainer.at(index)->get().mId; } template void RefIdDataContainer::save(int index, ESM::ESMWriter& writer) const { const Record& record = *mContainer.at(index); if (record.isModified() || record.mState == RecordBase::State_Deleted) { RecordT esmRecord = record.get(); 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: 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 mRecordContainers; void erase(const LocalIndex& index, int count); ///< Must not spill over into another type. ESM::RefId getRecordId(const LocalIndex& index) const; public: RefIdData(); LocalIndex globalToLocalIndex(int index) const; int localToGlobalIndex(const LocalIndex& index) const; LocalIndex searchId(const ESM::RefId& id) const; void erase(int index, int count); void insertRecord(std::unique_ptr record, CSMWorld::UniversalId::Type type, const ESM::RefId& id); const RecordBase& getRecord(const LocalIndex& index) const; RecordBase& getRecord(const LocalIndex& index); unsigned int getRecordFlags(const ESM::RefId& id) const; void appendRecord(UniversalId::Type type, const ESM::RefId& id, bool base); int getAppendIndex(UniversalId::Type type) const; void load(ESM::ESMReader& reader, bool base, UniversalId::Type type); 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 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; void copyTo(int index, RefIdData& target) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/regionmap.cpp000066400000000000000000000341101503074453300235650ustar00rootroot00000000000000#include "regionmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "data.hpp" #include "universalid.hpp" 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; 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"); mMaxLandHeight = landHeight; mDeleted = cell.isDeleted(); mRegion = cell2.mRegion; mName = cell2.mName; } CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex(const QModelIndex& index) const { return mMin.move(index.column(), mMax.getY() - mMin.getY() - index.row() - 1); } 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()); } CSMWorld::CellCoordinates CSMWorld::RegionMap::getIndex(const Cell& cell) const { std::istringstream stream(cell.mId.getRefIdString()); char ignore; int x = 0; int y = 0; stream >> ignore >> x >> y; return CellCoordinates(x, y); } void CSMWorld::RegionMap::buildRegions() { const IdCollection& regions = mData.getRegions(); int size = regions.getSize(); for (int i = 0; i < size; ++i) { const Record& region = regions.getRecord(i); if (!region.isDeleted()) mColours.insert(std::make_pair(region.get().mId, region.get().mMapColor)); } } void CSMWorld::RegionMap::buildMap() { const IdCollection& cells = mData.getCells(); int size = cells.getSize(); for (int i = 0; i < size; ++i) { const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellDescription description(cell, getLandHeight(cell2, mData)); CellCoordinates index = getIndex(cell2); mMap.insert(std::make_pair(index, description)); } } std::pair mapSize = getSize(); mMin = mapSize.first; mMax = mapSize.second; } void CSMWorld::RegionMap::addCell(const CellCoordinates& index, const CellDescription& description) { std::map::iterator cell = mMap.find(index); if (cell != mMap.end()) { cell->second = description; } else { updateSize(); mMap.insert(std::make_pair(index, description)); } QModelIndex index2 = getIndex(index); dataChanged(index2, index2); } void CSMWorld::RegionMap::addCells(int start, int end) { const IdCollection& cells = mData.getCells(); for (int i = start; i <= end; ++i) { const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellCoordinates index = getIndex(cell2); CellDescription description(cell, getLandHeight(cell.get(), mData)); addCell(index, description); } } } void CSMWorld::RegionMap::removeCell(const CellCoordinates& index) { std::map::iterator cell = mMap.find(index); if (cell != mMap.end()) { mMap.erase(cell); QModelIndex index2 = getIndex(index); dataChanged(index2, index2); updateSize(); } } void CSMWorld::RegionMap::addRegion(const ESM::RefId& region, unsigned int colour) { mColours[region] = colour; } void CSMWorld::RegionMap::removeRegion(const ESM::RefId& region) { auto iter(mColours.find(region)); if (iter != mColours.end()) mColours.erase(iter); } void CSMWorld::RegionMap::updateRegions(const std::vector& regions) { std::vector regions2(regions); std::sort(regions2.begin(), regions2.end()); for (std::map::const_iterator iter(mMap.begin()); iter != mMap.end(); ++iter) { if (!iter->second.mRegion.empty() && std::find(regions2.begin(), regions2.end(), iter->second.mRegion) != regions2.end()) { QModelIndex index = getIndex(iter->first); dataChanged(index, index); } } } void CSMWorld::RegionMap::updateSize() { std::pair size = getSize(); if (int diff = size.first.getX() - mMin.getX()) { 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()); endInsertRows(); } if (int diff = size.second.getX() - mMax.getX()) { int columns = columnCount(); if (diff > 0) beginInsertColumns(QModelIndex(), columns, columns + diff - 1); else beginRemoveColumns(QModelIndex(), columns + diff, columns - 1); mMax = CellCoordinates(size.second.getX(), mMax.getY()); endInsertColumns(); } if (int diff = size.second.getY() - mMax.getY()) { int rows = rowCount(); if (diff > 0) beginInsertRows(QModelIndex(), rows, rows + diff - 1); else beginRemoveRows(QModelIndex(), rows + diff, rows - 1); mMax = CellCoordinates(mMax.getX(), size.second.getY()); endInsertRows(); } } std::pair CSMWorld::RegionMap::getSize() const { const IdCollection& cells = mData.getCells(); int size = cells.getSize(); CellCoordinates min(0, 0); CellCoordinates max(0, 0); for (int i = 0; i < size; ++i) { const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) { CellCoordinates index = getIndex(cell2); if (min == max) { min = index; max = min.move(1, 1); } else { 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); } CSMWorld::RegionMap::RegionMap(Data& data) : mData(data) { buildRegions(); buildMap(); QAbstractItemModel* regions = data.getTableModel(UniversalId(UniversalId::Type_Regions)); 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)); 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 { if (parent.isValid()) return 0; return mMax.getY() - mMin.getY(); } int CSMWorld::RegionMap::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return mMax.getX() - mMin.getX(); } QVariant CSMWorld::RegionMap::data(const QModelIndex& index, int role) const { if (role == Qt::SizeHintRole) return QSize(16, 16); 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)); if (cell != mMap.end()) { if (cell->second.mDeleted) return QBrush(Qt::red, Qt::DiagCrossPattern); 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), cell->second.mMaxLandHeight > 0 ? Qt::SolidPattern : Qt::CrossPattern); 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::DiagCrossPattern); } if (role == Qt::ToolTipRole) { CellCoordinates cellIndex = getIndex(index); std::ostringstream stream; stream << cellIndex; std::map::const_iterator cell = mMap.find(cellIndex); if (cell != mMap.end()) { if (!cell->second.mName.empty()) stream << " " << cell->second.mName; if (cell->second.mDeleted) stream << " (deleted)"; if (!cell->second.mRegion.empty()) { stream << "
"; auto iter = mColours.find(cell->second.mRegion); if (iter != mColours.end()) stream << cell->second.mRegion; else stream << "" << cell->second.mRegion << ""; } } else stream << " (no cell)"; return QString::fromUtf8(stream.str().c_str()); } if (role == Role_Region) { CellCoordinates cellIndex = getIndex(index); std::map::const_iterator cell = mMap.find(cellIndex); if (cell != mMap.end() && !cell->second.mRegion.empty()) return QString::fromUtf8(cell->second.mRegion.getRefIdString().c_str()); } if (role == Role_CellId) { CellCoordinates cellIndex = getIndex(index); std::ostringstream stream; stream << "#" << cellIndex.getX() << " " << cellIndex.getY(); return QString::fromUtf8(stream.str().c_str()); } return QVariant(); } 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) { std::vector update; const IdCollection& regions = mData.getRegions(); for (int i = start; i <= end; ++i) { const Record& region = regions.getRecord(i); update.push_back(region.get().mId); removeRegion(region.get().mId); } updateRegions(update); } void CSMWorld::RegionMap::regionsInserted(const QModelIndex& parent, int start, int end) { std::vector update; const IdCollection& regions = mData.getRegions(); for (int i = start; i <= end; ++i) { const Record& region = regions.getRecord(i); if (!region.isDeleted()) { update.push_back(region.get().mId); addRegion(region.get().mId, region.get().mMapColor); } } updateRegions(update); } 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; const IdCollection& regions = mData.getRegions(); for (int i = topLeft.row(); i <= bottomRight.column(); ++i) { const Record& region = regions.getRecord(i); update.push_back(region.get().mId); if (!region.isDeleted()) addRegion(region.get().mId, region.get().mMapColor); else removeRegion(region.get().mId); } updateRegions(update); } void CSMWorld::RegionMap::cellsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const IdCollection& cells = mData.getCells(); for (int i = start; i <= end; ++i) { const Record& cell = cells.getRecord(i); const Cell& cell2 = cell.get(); if (cell2.isExterior()) removeCell(getIndex(cell2)); } } void CSMWorld::RegionMap::cellsInserted(const QModelIndex& parent, int start, int end) { addCells(start, end); } 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()); } openmw-openmw-0.49.0/apps/opencs/model/world/regionmap.hpp000066400000000000000000000072561503074453300236050ustar00rootroot00000000000000#ifndef CSM_WOLRD_REGIONMAP_H #define CSM_WOLRD_REGIONMAP_H #include #include #include #include #include #include #include #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 { float mMaxLandHeight; bool mDeleted; ESM::RefId mRegion; std::string mName; CellDescription(const Record& cell, float landHeight); }; Data& mData; std::map mMap; CellCoordinates mMin; ///< inclusive CellCoordinates mMax; ///< exclusive std::map mColours; ///< region ID, colour (RGBA) CellCoordinates getIndex(const QModelIndex& index) const; ///< Translates a Qt model index into a cell index (which can contain negative components) QModelIndex getIndex(const CellCoordinates& index) const; CellCoordinates getIndex(const Cell& cell) const; void buildRegions(); void buildMap(); 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 addCells(int start, int end); 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 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 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 updateRegions(const std::vector& regions); ///< Update cells affected by the listed regions void updateSize(); std::pair getSize() const; public: RegionMap(Data& data); int rowCount(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. Qt::ItemFlags flags(const QModelIndex& index) const override; private slots: void regionsAboutToBeRemoved(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 cellsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void cellsInserted(const QModelIndex& parent, int start, int end); void cellsChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/resources.cpp000066400000000000000000000047741503074453300236330ustar00rootroot00000000000000#include "resources.hpp" #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) { recreate(vfs, extensions); } void CSMWorld::Resources::recreate(const VFS::Manager* vfs, const char* const* extensions) { mFiles.clear(); mIndex.clear(); size_t baseSize = mBaseDirectory.size(); for (const auto& filepath : vfs->getRecursiveDirectoryIterator()) { const std::string_view view = filepath.view(); if (view.size() < baseSize + 1 || !view.starts_with(mBaseDirectory) || view[baseSize] != '/') continue; if (extensions) { const auto extensionIndex = view.find_last_of('.'); if (extensionIndex == std::string_view::npos) continue; std::string_view extension = view.substr(extensionIndex + 1); int i = 0; for (; extensions[i]; ++i) if (extensions[i] == extension) break; if (!extensions[i]) continue; } std::string file(view.substr(baseSize + 1)); mFiles.push_back(file); mIndex.emplace(std::move(file), static_cast(mFiles.size()) - 1); } } int CSMWorld::Resources::getSize() const { return static_cast(mFiles.size()); } std::string CSMWorld::Resources::getId(int index) const { return mFiles.at(index); } int CSMWorld::Resources::getIndex(const std::string& id) const { int index = searchId(id); if (index == -1) { std::ostringstream stream; stream << "Invalid resource: " << mBaseDirectory << '/' << id; throw std::runtime_error(stream.str()); } return index; } int CSMWorld::Resources::searchId(std::string_view id) const { std::string id2 = Misc::StringUtils::lowerCase(id); std::replace(id2.begin(), id2.end(), '\\', '/'); std::map::const_iterator iter = mIndex.find(id2); if (iter == mIndex.end()) return -1; return iter->second; } CSMWorld::UniversalId::Type CSMWorld::Resources::getType() const { return mType; } openmw-openmw-0.49.0/apps/opencs/model/world/resources.hpp000066400000000000000000000016731503074453300236330ustar00rootroot00000000000000#ifndef CSM_WOLRD_RESOURCES_H #define CSM_WOLRD_RESOURCES_H #include #include #include #include #include "universalid.hpp" namespace VFS { class Manager; } namespace CSMWorld { class Resources { std::map mIndex; std::vector mFiles; std::string mBaseDirectory; UniversalId::Type mType; 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); int getSize() const; std::string getId(int index) const; int getIndex(const std::string& id) const; int searchId(std::string_view id) const; UniversalId::Type getType() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/resourcesmanager.cpp000066400000000000000000000041121503074453300251500ustar00rootroot00000000000000#include "resourcesmanager.hpp" #include "resources.hpp" #include #include #include #include CSMWorld::ResourcesManager::ResourcesManager() : mVFS(nullptr) { } 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)); } 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 }; return sMeshTypes; } 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)); } const VFS::Manager* CSMWorld::ResourcesManager::getVFS() const { return mVFS; } void CSMWorld::ResourcesManager::recreateResources() { std::map::iterator it = mResources.begin(); for (; it != mResources.end(); ++it) { if (it->first == UniversalId::Type_Mesh) it->second.recreate(mVFS, getMeshExtensions()); else it->second.recreate(mVFS); } } const CSMWorld::Resources& CSMWorld::ResourcesManager::get(UniversalId::Type type) const { std::map::const_iterator iter = mResources.find(type); if (iter == mResources.end()) throw std::logic_error("Unknown resource type"); return iter->second; } openmw-openmw-0.49.0/apps/opencs/model/world/resourcesmanager.hpp000066400000000000000000000013531503074453300251610ustar00rootroot00000000000000#ifndef CSM_WOLRD_RESOURCESMANAGER_H #define CSM_WOLRD_RESOURCESMANAGER_H #include #include #include "universalid.hpp" namespace VFS { class Manager; } namespace CSMWorld { class ResourcesManager { std::map mResources; const VFS::Manager* mVFS; private: void addResources(const Resources& resources); const char* const* getMeshExtensions(); public: ResourcesManager(); ~ResourcesManager(); const VFS::Manager* getVFS() const; void setVFS(const VFS::Manager* vfs); void recreateResources(); const Resources& get(UniversalId::Type type) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/resourcetable.cpp000066400000000000000000000074511503074453300244530ustar00rootroot00000000000000#include "resourcetable.hpp" #include #include #include #include "columnbase.hpp" #include "resources.hpp" #include "universalid.hpp" CSMWorld::ResourceTable::ResourceTable(const Resources* resources, unsigned int features) : IdTableBase(features | Feature_Constant) , mResources(resources) { } int CSMWorld::ResourceTable::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return mResources->getSize(); } int CSMWorld::ResourceTable::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return 2; // ID, type } QVariant CSMWorld::ResourceTable::data(const QModelIndex& index, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (index.column() == 0) return QString::fromUtf8(mResources->getId(index.row()).c_str()); if (index.column() == 1) return static_cast(mResources->getType()); throw std::logic_error("Invalid column in resource table"); } QVariant CSMWorld::ResourceTable::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Vertical) return QVariant(); 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 == ColumnBase::Role_Display) return ColumnBase::Display_Id; break; case 1: if (role == Qt::DisplayRole) return Columns::getName(Columns::ColumnId_RecordType).c_str(); if (role == ColumnBase::Role_Display) return ColumnBase::Display_Integer; break; } return QVariant(); } bool CSMWorld::ResourceTable::setData(const QModelIndex& index, const QVariant& value, int role) { return false; } 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 { if (parent.isValid()) return QModelIndex(); if (row < 0 || row >= mResources->getSize()) return QModelIndex(); if (column < 0 || column > 1) return QModelIndex(); return createIndex(row, column); } QModelIndex CSMWorld::ResourceTable::parent(const QModelIndex& index) const { return QModelIndex(); } QModelIndex CSMWorld::ResourceTable::getModelIndex(const std::string& id, int column) const { int row = mResources->searchId(id); if (row != -1) return index(row, column); return QModelIndex(); } int CSMWorld::ResourceTable::searchColumnIndex(Columns::ColumnId id) const { if (id == Columns::ColumnId_Id) return 0; if (id == Columns::ColumnId_RecordType) return 1; return -1; } int CSMWorld::ResourceTable::findColumnIndex(Columns::ColumnId id) const { int index = searchColumnIndex(id); if (index == -1) throw std::logic_error("invalid column index"); return index; } std::pair CSMWorld::ResourceTable::view(int row) const { return std::make_pair(UniversalId::Type_None, ""); } bool CSMWorld::ResourceTable::isDeleted(const std::string& id) const { return false; } int CSMWorld::ResourceTable::getColumnId(int column) const { switch (column) { case 0: return Columns::ColumnId_Id; case 1: return Columns::ColumnId_RecordType; } return -1; } void CSMWorld::ResourceTable::beginReset() { beginResetModel(); } void CSMWorld::ResourceTable::endReset() { endResetModel(); } openmw-openmw-0.49.0/apps/opencs/model/world/resourcetable.hpp000066400000000000000000000044461503074453300244610ustar00rootroot00000000000000#ifndef CSM_WOLRD_RESOURCETABLE_H #define CSM_WOLRD_RESOURCETABLE_H #include "idtablebase.hpp" #include #include #include #include #include namespace CSMWorld { class Resources; class UniversalId; class ResourceTable : public IdTableBase { const Resources* mResources; public: /// \note The feature Feature_Constant will be added implicitly. ResourceTable(const Resources* resources, unsigned int features = 0); ~ResourceTable() override = default; int rowCount(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 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; Qt::ItemFlags flags(const QModelIndex& index) const override; QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex& index) 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, 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; /// Is \a id flagged as deleted? bool isDeleted(const std::string& id) 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/scope.cpp000066400000000000000000000020471503074453300227210ustar00rootroot00000000000000#include "scope.hpp" #include #include #include namespace CSMWorld { 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; } }; } } CSMWorld::Scope CSMWorld::getScopeFromId(ESM::RefId id) { return visit(GetScope{}, id); } openmw-openmw-0.49.0/apps/opencs/model/world/scope.hpp000066400000000000000000000006551503074453300227310ustar00rootroot00000000000000#ifndef CSM_WOLRD_SCOPE_H #define CSM_WOLRD_SCOPE_H namespace ESM { class RefId; } namespace CSMWorld { enum Scope { // record stored in content file Scope_Content = 1, // record stored in project file Scope_Project = 2, // record that exists only for the duration of one editing session Scope_Session = 4 }; Scope getScopeFromId(ESM::RefId id); } #endif openmw-openmw-0.49.0/apps/opencs/model/world/scriptcontext.cpp000066400000000000000000000070761503074453300245300ustar00rootroot00000000000000#include "scriptcontext.hpp" #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) { } bool CSMWorld::ScriptContext::canDeclareLocals() const { return true; } char CSMWorld::ScriptContext::getGlobalType(const std::string& name) const { const int index = mData.getGlobals().searchId(ESM::RefId::stringRefId(name)); if (index != -1) { 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 ' '; } } return ' '; } std::pair CSMWorld::ScriptContext::getMemberType(const std::string& name, const ESM::RefId& id) const { ESM::RefId id2 = id; int index = mData.getScripts().searchId(id2); bool reference = false; if (index == -1) { // ID is not a script ID. Search for a matching referenceable instead. index = mData.getReferenceables().searchId(id2); if (index != -1) { // Referenceable found. int columnIndex = mData.getReferenceables().findColumnIndex(Columns::ColumnId_Script); 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); reference = true; } } } if (index == -1) return std::make_pair(' ', false); auto iter = mLocals.find(id2); 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); iter = mLocals.emplace(id2, std::move(locals)).first; } return std::make_pair(iter->second.getType(Misc::StringUtils::lowerCase(name)), reference); } bool CSMWorld::ScriptContext::isId(const ESM::RefId& name) const { if (!mIdsUpdated) { mIds = mData.getIds(); std::sort(mIds.begin(), mIds.end()); mIdsUpdated = true; } return std::binary_search(mIds.begin(), mIds.end(), name); } void CSMWorld::ScriptContext::invalidateIds() { mIdsUpdated = false; } void CSMWorld::ScriptContext::clear() { mIds.clear(); mIdsUpdated = false; mLocals.clear(); } bool CSMWorld::ScriptContext::clearLocals(const std::string& script) { const auto iter = mLocals.find(script); if (iter != mLocals.end()) { mLocals.erase(iter); mIdsUpdated = false; return true; } return false; } openmw-openmw-0.49.0/apps/opencs/model/world/scriptcontext.hpp000066400000000000000000000030161503074453300245230ustar00rootroot00000000000000#ifndef CSM_WORLD_SCRIPTCONTEXT_H #define CSM_WORLD_SCRIPTCONTEXT_H #include #include #include #include #include #include #include namespace CSMWorld { class Data; class ScriptContext : public Compiler::Context { const Data& mData; mutable std::vector mIds; mutable bool mIdsUpdated; mutable std::map> mLocals; public: ScriptContext(const Data& data); bool canDeclareLocals() const override; ///< Is the compiler allowed to declare local variables? char getGlobalType(const std::string& name) const override; ///< 'l: long, 's': short, 'f': float, ' ': does not exist. 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? void invalidateIds(); void clear(); ///< Remove all cached data. /// \return Were there any locals that needed clearing? bool clearLocals(const std::string& script); }; } #endif openmw-openmw-0.49.0/apps/opencs/model/world/subcellcollection.hpp000066400000000000000000000017651503074453300253300ustar00rootroot00000000000000#ifndef CSM_WOLRD_SUBCOLLECTION_H #define CSM_WOLRD_SUBCOLLECTION_H #include "nestedidcollection.hpp" namespace ESM { class ESMReader; } namespace CSMWorld { struct Cell; /// \brief Single type collection of top level records that are associated with cells template class SubCellCollection final : public NestedIdCollection { const IdCollection& mCells; void loadRecord(ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) override; public: SubCellCollection(const IdCollection& cells); }; template void SubCellCollection::loadRecord( ESXRecordT& record, ESM::ESMReader& reader, bool& isDeleted, bool base) { record.load(reader, isDeleted, mCells); } template SubCellCollection::SubCellCollection(const IdCollection& cells) : mCells(cells) { } } #endif openmw-openmw-0.49.0/apps/opencs/model/world/tablemimedata.cpp000066400000000000000000000275331503074453300244100ustar00rootroot00000000000000#include "tablemimedata.hpp" #include #include #include #include #include #include #include "columnbase.hpp" #include "universalid.hpp" 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()); } 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) { mObjectsFormats << QString::fromUtf8(("tabledata/" + it->getTypeName()).c_str()); } } QStringList CSMWorld::TableMimeData::formats() const { return mObjectsFormats; } 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!"); } std::string tmpIcon; bool firstIteration = true; for (const auto& id : mUniversalId) { if (firstIteration) { firstIteration = false; tmpIcon = id.getIcon(); continue; } if (tmpIcon != id.getIcon()) { return ":multitype"; } tmpIcon = id.getIcon(); } return mUniversalId.begin()->getIcon(); // All objects are of the same type; } 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); } 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); } 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) { if (referencable) { if (isReferencable(it->getType())) { return true; } } else { if (it->getType() == type) { return true; } } } return false; } 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) { if (referencable) { if (isReferencable(it->getType())) { return true; } } else { if (it->getType() == convertEnums(type)) { return true; } } } return false; } 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) { if (referencable) { if (isReferencable(it->getType())) { return *it; } } else { if (it->getType() == type) { return *it; } } } throw std::runtime_error("TableMimeData object does not hold object of the sought type"); } 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) { if (referencable) { if (isReferencable(it->getType())) { return *it; } } else { if (it->getType() == convertEnums(type)) { return *it; } } } throw std::runtime_error("TableMimeData object does not hold object of the sought type"); } bool CSMWorld::TableMimeData::fromDocument(const CSMDoc::Document& document) const { return &document == &mDocument; } namespace { struct Mapping { CSMWorld::UniversalId::Type mUniversalIdType; CSMWorld::ColumnBase::Display mDisplayType; }; 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 }, { CSMWorld::UniversalId::Type_Faction, CSMWorld::ColumnBase::Display_Faction }, { CSMWorld::UniversalId::Type_Sound, CSMWorld::ColumnBase::Display_Sound }, { CSMWorld::UniversalId::Type_Region, CSMWorld::ColumnBase::Display_Region }, { CSMWorld::UniversalId::Type_Birthsign, CSMWorld::ColumnBase::Display_Birthsign }, { CSMWorld::UniversalId::Type_Spell, CSMWorld::ColumnBase::Display_Spell }, { CSMWorld::UniversalId::Type_Cell, CSMWorld::ColumnBase::Display_Cell }, { CSMWorld::UniversalId::Type_Referenceable, CSMWorld::ColumnBase::Display_Referenceable }, { CSMWorld::UniversalId::Type_Activator, CSMWorld::ColumnBase::Display_Activator }, { CSMWorld::UniversalId::Type_Potion, CSMWorld::ColumnBase::Display_Potion }, { CSMWorld::UniversalId::Type_Apparatus, CSMWorld::ColumnBase::Display_Apparatus }, { CSMWorld::UniversalId::Type_Armor, CSMWorld::ColumnBase::Display_Armor }, { CSMWorld::UniversalId::Type_Book, CSMWorld::ColumnBase::Display_Book }, { CSMWorld::UniversalId::Type_Clothing, CSMWorld::ColumnBase::Display_Clothing }, { CSMWorld::UniversalId::Type_Container, CSMWorld::ColumnBase::Display_Container }, { CSMWorld::UniversalId::Type_Creature, CSMWorld::ColumnBase::Display_Creature }, { CSMWorld::UniversalId::Type_Door, CSMWorld::ColumnBase::Display_Door }, { CSMWorld::UniversalId::Type_Ingredient, CSMWorld::ColumnBase::Display_Ingredient }, { CSMWorld::UniversalId::Type_CreatureLevelledList, CSMWorld::ColumnBase::Display_CreatureLevelledList }, { CSMWorld::UniversalId::Type_ItemLevelledList, CSMWorld::ColumnBase::Display_ItemLevelledList }, { CSMWorld::UniversalId::Type_Light, CSMWorld::ColumnBase::Display_Light }, { CSMWorld::UniversalId::Type_Lockpick, CSMWorld::ColumnBase::Display_Lockpick }, { CSMWorld::UniversalId::Type_Miscellaneous, CSMWorld::ColumnBase::Display_Miscellaneous }, { CSMWorld::UniversalId::Type_Npc, CSMWorld::ColumnBase::Display_Npc }, { CSMWorld::UniversalId::Type_Probe, CSMWorld::ColumnBase::Display_Probe }, { CSMWorld::UniversalId::Type_Repair, CSMWorld::ColumnBase::Display_Repair }, { CSMWorld::UniversalId::Type_Static, CSMWorld::ColumnBase::Display_Static }, { CSMWorld::UniversalId::Type_Weapon, CSMWorld::ColumnBase::Display_Weapon }, { CSMWorld::UniversalId::Type_Reference, CSMWorld::ColumnBase::Display_Reference }, { CSMWorld::UniversalId::Type_Filter, CSMWorld::ColumnBase::Display_Filter }, { CSMWorld::UniversalId::Type_Topic, CSMWorld::ColumnBase::Display_Topic }, { CSMWorld::UniversalId::Type_Journal, CSMWorld::ColumnBase::Display_Journal }, { CSMWorld::UniversalId::Type_TopicInfo, CSMWorld::ColumnBase::Display_TopicInfo }, { CSMWorld::UniversalId::Type_JournalInfo, CSMWorld::ColumnBase::Display_JournalInfo }, { CSMWorld::UniversalId::Type_Scene, CSMWorld::ColumnBase::Display_Scene }, { CSMWorld::UniversalId::Type_Script, CSMWorld::ColumnBase::Display_Script }, { CSMWorld::UniversalId::Type_Mesh, CSMWorld::ColumnBase::Display_Mesh }, { CSMWorld::UniversalId::Type_Icon, CSMWorld::ColumnBase::Display_Icon }, { CSMWorld::UniversalId::Type_Music, CSMWorld::ColumnBase::Display_Music }, { CSMWorld::UniversalId::Type_SoundRes, CSMWorld::ColumnBase::Display_SoundRes }, { CSMWorld::UniversalId::Type_Texture, CSMWorld::ColumnBase::Display_Texture }, { CSMWorld::UniversalId::Type_Video, CSMWorld::ColumnBase::Display_Video }, { CSMWorld::UniversalId::Type_Global, CSMWorld::ColumnBase::Display_GlobalVariable }, { CSMWorld::UniversalId::Type_BodyPart, CSMWorld::ColumnBase::Display_BodyPart }, { CSMWorld::UniversalId::Type_Enchantment, CSMWorld::ColumnBase::Display_Enchantment }, { CSMWorld::UniversalId::Type_None, CSMWorld::ColumnBase::Display_None } // end marker }; } 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) return mapping[i].mUniversalIdType; return CSMWorld::UniversalId::Type_None; } 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) return mapping[i].mDisplayType; return CSMWorld::ColumnBase::Display_None; } const CSMDoc::Document* CSMWorld::TableMimeData::getDocumentPtr() const { return &mDocument; } openmw-openmw-0.49.0/apps/opencs/model/world/tablemimedata.hpp000066400000000000000000000050301503074453300244010ustar00rootroot00000000000000#ifndef TABLEMIMEDATA_H #define TABLEMIMEDATA_H #include #include #include #include #include #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. class TableMimeData : public QMimeData { 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; std::vector getData() const; bool holdsType(UniversalId::Type type) const; bool holdsType(CSMWorld::ColumnBase::Display type) const; bool fromDocument(const CSMDoc::Document& document) const; UniversalId returnMatching(UniversalId::Type type) const; const CSMDoc::Document* getDocumentPtr() const; UniversalId returnMatching(CSMWorld::ColumnBase::Display type) const; void setIndexAtDragStart(const QModelIndex& index) { mIndexAtDragStart = index; } void setTableOfDragStart(const CSVWorld::DragRecordTable* const table) { mTableOfDragStart = table; } const QModelIndex getIndexAtDragStart() const { return mIndexAtDragStart; } const CSVWorld::DragRecordTable* getTableOfDragStart() const { return mTableOfDragStart; } static CSMWorld::UniversalId::Type convertEnums(CSMWorld::ColumnBase::Display 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; }; } #endif // TABLEMIMEDATA_H openmw-openmw-0.49.0/apps/opencs/model/world/universalid.cpp000066400000000000000000000523671503074453300241470ustar00rootroot00000000000000#include "universalid.hpp" #include #include #include #include #include #include #include #include #include namespace { struct TypeData { CSMWorld::UniversalId::Class mClass; CSMWorld::UniversalId::Type mType; std::string_view mName; std::string_view mIcon; }; 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" }, }; 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" }, { 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" }, }; struct WriteToStream { 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) : mValue(std::monostate{}) { std::string::size_type index = universalId.find(':'); if (index != std::string::npos) { std::string type = universalId.substr(0, index); for (const TypeData& value : sIdArg) if (type == value.mName) { mType = value.mType; mClass = value.mClass; mValue = universalId.substr(index + 2); return; } for (const TypeData& value : sIndexArg) if (type == value.mName) { mType = value.mType; mClass = value.mClass; std::istringstream stream(universalId.substr(index + 2)); int index = 0; if (stream >> index) { mValue = index; return; } break; } } else { for (const TypeData& value : sIndexArg) if (universalId == value.mName) { mType = value.mType; mClass = value.mClass; return; } } throw std::runtime_error("invalid UniversalId: " + universalId); } CSMWorld::UniversalId::UniversalId(Type type) : mType(type) , mValue(std::monostate{}) { for (const TypeData& value : sNoArg) if (type == value.mType) { mClass = value.mClass; return; } for (const TypeData& value : sIdArg) if (type == value.mType) { mValue = std::string(); mClass = value.mClass; return; } for (const TypeData& value : sIndexArg) if (type == value.mType) { mValue = int{}; mClass = value.mClass; return; } throw std::logic_error("invalid argument-less UniversalId type"); } CSMWorld::UniversalId::UniversalId(Type type, const std::string& id) : mType(type) , mValue(id) { for (const TypeData& value : sIdArg) if (type == value.mType) { mClass = value.mClass; return; } throw std::logic_error("invalid ID argument UniversalId type: " + std::to_string(type)); } CSMWorld::UniversalId::UniversalId(Type type, ESM::RefId id) : mType(type) , mValue(id) { for (const TypeData& value : sIdArg) if (type == value.mType) { mClass = value.mClass; return; } throw std::logic_error("invalid RefId argument UniversalId type: " + std::to_string(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 { return mClass; } CSMWorld::UniversalId::ArgumentType CSMWorld::UniversalId::getArgumentType() const { return static_cast(mValue.index()); } CSMWorld::UniversalId::Type CSMWorld::UniversalId::getType() const { return mType; } const std::string& CSMWorld::UniversalId::getId() const { 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(); throw std::logic_error("invalid access to ID of " + ::toString(getArgumentType()) + " UniversalId"); } int CSMWorld::UniversalId::getIndex() const { if (const int* result = std::get_if(&mValue)) return *result; throw std::logic_error("invalid access to index of " + ::toString(getArgumentType()) + " UniversalId"); } ESM::RefId CSMWorld::UniversalId::getRefId() const { if (const ESM::RefId* result = std::get_if(&mValue)) return *result; throw std::logic_error("invalid access to RefId of " + ::toString(getArgumentType()) + " UniversalId"); } std::string CSMWorld::UniversalId::getTypeName() const { const std::span typeData = std::visit(GetTypeData{}, mValue); for (const TypeData& value : typeData) if (value.mType == mType) return std::string(value.mName); throw std::logic_error("failed to retrieve UniversalId type name"); } std::string CSMWorld::UniversalId::toString() const { std::ostringstream stream; stream << getTypeName(); std::visit(WriteToStream{ stream }, mValue); return stream.str(); } std::string CSMWorld::UniversalId::getIcon() const { const std::span typeData = std::visit(GetTypeData{}, mValue); for (const TypeData& value : typeData) if (value.mType == mType) return std::string(value.mIcon); throw std::logic_error("failed to retrieve UniversalId type icon"); } std::vector CSMWorld::UniversalId::listReferenceableTypes() { std::vector list; 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 list; for (const TypeData& value : sNoArg) if (value.mClass & classes) list.push_back(value.mType); for (const TypeData& value : sIdArg) if (value.mClass & classes) list.push_back(value.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) { for (const TypeData& value : sIdArg) if (type == value.mType) { if (value.mClass == Class_RefRecord) return Type_Referenceables; if (value.mClass == Class_SubRecord || value.mClass == Class_Record || value.mClass == Class_Resource) { if (type == Type_Cell_Missing) return Type_Cells; return static_cast(type - 1); } break; } return Type_None; } bool CSMWorld::operator==(const CSMWorld::UniversalId& left, const CSMWorld::UniversalId& right) { return std::tie(left.mClass, left.mType, left.mValue) == std::tie(right.mClass, right.mType, right.mValue); } bool CSMWorld::operator<(const UniversalId& left, const UniversalId& right) { return std::tie(left.mClass, left.mType, left.mValue) < std::tie(right.mClass, right.mType, right.mValue); } openmw-openmw-0.49.0/apps/opencs/model/world/universalid.hpp000066400000000000000000000135231503074453300241430ustar00rootroot00000000000000#ifndef CSM_WOLRD_UNIVERSALID_H #define CSM_WOLRD_UNIVERSALID_H #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, 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); } Q_DECLARE_METATYPE(CSMWorld::UniversalId) #endif openmw-openmw-0.49.0/apps/opencs/ui/000077500000000000000000000000001503074453300172675ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/ui/filedialog.ui000066400000000000000000000037041503074453300217310ustar00rootroot00000000000000 FileDialog 0 0 518 109 0 0 Qt::DefaultContextMenu Qt::NoFocus Project Name false 6 3 6 0 6 QDialogButtonBox::Cancel|QDialogButtonBox::Ok openmw-openmw-0.49.0/apps/opencs/view/000077500000000000000000000000001503074453300176245ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/doc/000077500000000000000000000000001503074453300203715ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/doc/adjusterwidget.cpp000066400000000000000000000063221503074453300241250ustar00rootroot00000000000000#include "adjusterwidget.hpp" #include #include #include #include #include #include #include #include CSVDoc::AdjusterWidget::AdjusterWidget(QWidget* parent) : QWidget(parent) , mValid(false) , mAction(ContentAction_Undefined) { QHBoxLayout* layout = new QHBoxLayout(this); mIcon = new QLabel(this); layout->addWidget(mIcon, 0); mMessage = new QLabel(this); mMessage->setWordWrap(true); mMessage->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); layout->addWidget(mMessage, 1); setName("", false); setLayout(layout); } void CSVDoc::AdjusterWidget::setAction(ContentAction action) { mAction = action; } void CSVDoc::AdjusterWidget::setLocalData(const std::filesystem::path& localData) { mLocalData = localData; } std::filesystem::path CSVDoc::AdjusterWidget::getPath() const { if (!mValid) throw std::logic_error("invalid content file path"); return mResultPath; } bool CSVDoc::AdjusterWidget::isValid() const { return mValid; } void CSVDoc::AdjusterWidget::setFilenameCheck(bool doCheck) { mDoFilenameCheck = doCheck; } void CSVDoc::AdjusterWidget::setName(const QString& name, bool addon) { QString message; mValid = (!name.isEmpty()); bool warning = false; if (!mValid) { message = "No name."; } else { auto path = Files::pathFromQString(name); const auto extension = Misc::StringUtils::lowerCase(path.extension().u8string()); bool isLegacyPath = (extension == u8".esm" || extension == u8".esp"); bool isFilePathChanged = (path.parent_path() != mLocalData); if (isLegacyPath) 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 (!isFilePathChanged && !isLegacyPath) { // path already points to the local data directory 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 else { // path points somewhere else or is a leaf name. if (isFilePathChanged) path = mLocalData / path.filename(); message = "Will be saved as: " + Files::pathToQString(path); mResultPath = 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."; warning = true; } } } 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); } openmw-openmw-0.49.0/apps/opencs/view/doc/adjusterwidget.hpp000066400000000000000000000020741503074453300241320ustar00rootroot00000000000000#ifndef CSV_DOC_ADJUSTERWIDGET_H #define CSV_DOC_ADJUSTERWIDGET_H #include #include class QLabel; namespace CSVDoc { enum ContentAction { ContentAction_New, ContentAction_Edit, ContentAction_Undefined }; class AdjusterWidget : public QWidget { Q_OBJECT public: std::filesystem::path mLocalData; QLabel* mMessage; QLabel* mIcon; bool mValid; std::filesystem::path mResultPath; ContentAction mAction; bool mDoFilenameCheck; public: AdjusterWidget(QWidget* parent = nullptr); void setLocalData(const std::filesystem::path& localData); void setAction(ContentAction action); void setFilenameCheck(bool doCheck); bool isValid() const; std::filesystem::path getPath() const; ///< This function must not be called if there is no valid path. public slots: void setName(const QString& name, bool addon); signals: void stateChanged(bool valid); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/filedialog.cpp000066400000000000000000000141641503074453300232020ustar00rootroot00000000000000#include "filedialog.hpp" #include #include #include #include #include #include #include #include "adjusterwidget.hpp" #include "filewidget.hpp" #include "ui_filedialog.h" CSVDoc::FileDialog::FileDialog(QWidget* parent) : QDialog(parent) , mSelector(nullptr) , ui(std::make_unique()) , mAction(ContentAction_Undefined) , mFileWidget(nullptr) , mAdjusterWidget(nullptr) , mDialogBuilt(false) { ui->setupUi(this); resize(400, 400); setObjectName("FileDialog"); mSelector = new ContentSelectorView::ContentSelector(ui->contentSelectorWidget, /*showOMWScripts=*/false); mAdjusterWidget = new AdjusterWidget(this); } CSVDoc::FileDialog::~FileDialog() = default; void CSVDoc::FileDialog::addFiles(const std::vector& dataDirs) { for (const auto& dir : dataDirs) { QString path = Files::pathToQString(dir); mSelector->addFiles(path); } mSelector->sortFiles(); } void CSVDoc::FileDialog::setEncoding(const QString& encoding) { mSelector->setEncoding(encoding); } void CSVDoc::FileDialog::clearFiles() { mSelector->clearFiles(); } QStringList CSVDoc::FileDialog::selectedFilePaths() { QStringList filePaths; for (ContentSelectorModel::EsmFile* file : mSelector->selectedFiles()) filePaths.append(file->filePath()); return filePaths; } void CSVDoc::FileDialog::setLocalData(const std::filesystem::path& localData) { mAdjusterWidget->setLocalData(localData); } void CSVDoc::FileDialog::showDialog(ContentAction action) { mAction = action; ui->projectGroupBoxLayout->insertWidget(0, mAdjusterWidget); switch (mAction) { case ContentAction_New: buildNewFileView(); break; case ContentAction_Edit: buildOpenFileView(); break; default: break; } mAdjusterWidget->setFilenameCheck(mAction == ContentAction_New); if (!mDialogBuilt) { // connections common to both dialog view flavors connect(mSelector, &ContentSelectorView::ContentSelector::signalCurrentGamefileIndexChanged, this, qOverload(&FileDialog::slotUpdateAcceptButton)); connect(ui->projectButtonBox, &QDialogButtonBox::rejected, this, &FileDialog::slotRejected); mDialogBuilt = true; } show(); raise(); activateWindow(); } void CSVDoc::FileDialog::buildNewFileView() { setWindowTitle(tr("Create a new addon")); QPushButton* createButton = ui->projectButtonBox->button(QDialogButtonBox::Ok); createButton->setText("Create"); createButton->setEnabled(false); if (!mFileWidget) { mFileWidget = new FileWidget(this); mFileWidget->setType(true); mFileWidget->extensionLabelIsVisible(true); connect(mFileWidget, &FileWidget::nameChanged, mAdjusterWidget, &AdjusterWidget::setName); connect(mFileWidget, &FileWidget::nameChanged, this, qOverload(&FileDialog::slotUpdateAcceptButton)); } ui->projectGroupBoxLayout->insertWidget(0, mFileWidget); 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); else ui->projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(false); if (!mDialogBuilt) { connect(mSelector, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this, &FileDialog::slotAddonDataChanged); } connect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); } void CSVDoc::FileDialog::slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright) { slotUpdateAcceptButton(0); } void CSVDoc::FileDialog::slotUpdateAcceptButton(int) { QString name = ""; if (mFileWidget && mAction == ContentAction_New) name = mFileWidget->getName(); slotUpdateAcceptButton(name, true); } void CSVDoc::FileDialog::slotUpdateAcceptButton(const QString& name, bool) { bool success = !mSelector->selectedFiles().empty(); bool isNew = (mAction == ContentAction_New); if (isNew) success = !name.isEmpty(); else if (success) { ContentSelectorModel::EsmFile* file = mSelector->selectedFiles().back(); mAdjusterWidget->setName(file->filePath(), !file->isGameFile()); } else mAdjusterWidget->setName("", true); ui->projectButtonBox->button(QDialogButtonBox::Ok)->setEnabled(success); } QString CSVDoc::FileDialog::filename() const { if (mAction == ContentAction_New) return ""; return mSelector->currentFile(); } void CSVDoc::FileDialog::slotRejected() { emit rejected(); disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); if (mFileWidget) { delete mFileWidget; mFileWidget = nullptr; } close(); } void CSVDoc::FileDialog::slotNewFile() { emit signalCreateNewFile(mAdjusterWidget->getPath()); if (mFileWidget) { delete mFileWidget; mFileWidget = nullptr; } disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotNewFile); close(); } void CSVDoc::FileDialog::slotOpenFile() { ContentSelectorModel::EsmFile* file = mSelector->selectedFiles().back(); mAdjusterWidget->setName(file->filePath(), !file->isGameFile()); emit signalOpenFiles(mAdjusterWidget->getPath()); disconnect(ui->projectButtonBox, &QDialogButtonBox::accepted, this, &FileDialog::slotOpenFile); close(); } openmw-openmw-0.49.0/apps/opencs/view/doc/filedialog.hpp000066400000000000000000000034471503074453300232110ustar00rootroot00000000000000#ifndef FILEDIALOG_HPP #define FILEDIALOG_HPP #include #ifndef Q_MOC_RUN #include "adjusterwidget.hpp" #ifndef CS_QT_STD_FILESYSTEM_PATH_DECLARED #define CS_QT_STD_FILESYSTEM_PATH_DECLARED Q_DECLARE_METATYPE(std::filesystem::path) #endif #endif #include #include class QModelIndex; namespace ContentSelectorView { class ContentSelector; } namespace Ui { class FileDialog; } namespace CSVDoc { class FileWidget; class FileDialog : public QDialog { Q_OBJECT private: ContentSelectorView::ContentSelector* mSelector; std::unique_ptr ui; ContentAction mAction; FileWidget* mFileWidget; AdjusterWidget* mAdjusterWidget; bool mDialogBuilt; public: explicit FileDialog(QWidget* parent = nullptr); ~FileDialog(); void showDialog(ContentAction action); void addFiles(const std::vector& dataDirs); void setEncoding(const QString& encoding); void clearFiles(); QString filename() const; QStringList selectedFilePaths(); void setLocalData(const std::filesystem::path& localData); private: void buildNewFileView(); void buildOpenFileView(); signals: void signalOpenFiles(const std::filesystem::path& path); void signalCreateNewFile(const std::filesystem::path& path); void signalUpdateAcceptButton(bool, int); private slots: void slotNewFile(); void slotOpenFile(); void slotUpdateAcceptButton(int); void slotUpdateAcceptButton(const QString&, bool); void slotRejected(); void slotAddonDataChanged(const QModelIndex& topleft, const QModelIndex& bottomright); }; } #endif // FILEDIALOG_HPP openmw-openmw-0.49.0/apps/opencs/view/doc/filewidget.cpp000066400000000000000000000022731503074453300232240ustar00rootroot00000000000000#include "filewidget.hpp" #include #include #include QString CSVDoc::FileWidget::getExtension() const { return mAddon ? ".omwaddon" : ".omwgame"; } CSVDoc::FileWidget::FileWidget(QWidget* parent) : QWidget(parent) , mAddon(false) { QHBoxLayout* layout = new QHBoxLayout(this); mInput = new QLineEdit(this); layout->addWidget(mInput, 1); mType = new QLabel(this); layout->addWidget(mType); connect(mInput, &QLineEdit::textChanged, this, &FileWidget::textChanged); setLayout(layout); } void CSVDoc::FileWidget::setType(bool addon) { mAddon = addon; mType->setText(getExtension()); } QString CSVDoc::FileWidget::getName() const { QString text = mInput->text(); if (text.isEmpty()) return ""; return text + getExtension(); } void CSVDoc::FileWidget::textChanged(const QString& text) { emit nameChanged(getName(), mAddon); } void CSVDoc::FileWidget::extensionLabelIsVisible(bool visible) { mType->setVisible(visible); } void CSVDoc::FileWidget::setName(const std::string& text) { QString text2 = QString::fromUtf8(text.c_str()); mInput->setText(text2); textChanged(text2); } openmw-openmw-0.49.0/apps/opencs/view/doc/filewidget.hpp000066400000000000000000000013311503074453300232230ustar00rootroot00000000000000#ifndef CSV_DOC_FILEWIDGET_H #define CSV_DOC_FILEWIDGET_H #include #include #include class QLabel; class QLineEdit; namespace CSVDoc { class FileWidget : public QWidget { Q_OBJECT bool mAddon; QLineEdit* mInput; QLabel* mType; QString getExtension() const; public: FileWidget(QWidget* parent = nullptr); void setType(bool addon); QString getName() const; void extensionLabelIsVisible(bool visible); void setName(const std::string& text); private slots: void textChanged(const QString& text); signals: void nameChanged(const QString& file, bool addon); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/globaldebugprofilemenu.cpp000066400000000000000000000052341503074453300256160ustar00rootroot00000000000000#include "globaldebugprofilemenu.hpp" #include #include #include #include #include #include #include #include "../../model/world/idtable.hpp" #include "../../model/world/record.hpp" class QWidget; void CSVDoc::GlobalDebugProfileMenu::rebuild() { clear(); 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 size = mDebugProfiles->rowCount(); std::vector ids; for (int i = 0; i < size; ++i) { int state = mDebugProfiles->data(mDebugProfiles->index(i, stateColumn)).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()); } mActions = new QActionGroup(this); connect(mActions, &QActionGroup::triggered, this, &GlobalDebugProfileMenu::actionTriggered); std::sort(ids.begin(), ids.end()); for (std::vector::const_iterator iter(ids.begin()); iter != ids.end(); ++iter) { mActions->addAction(addAction(*iter)); } } CSVDoc::GlobalDebugProfileMenu::GlobalDebugProfileMenu(CSMWorld::IdTable* debugProfiles, QWidget* parent) : QMenu(parent) , mDebugProfiles(debugProfiles) , mActions(nullptr) { rebuild(); connect(mDebugProfiles, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &GlobalDebugProfileMenu::profileAboutToBeRemoved); connect(mDebugProfiles, &CSMWorld::IdTable::rowsInserted, this, &GlobalDebugProfileMenu::profileInserted); connect(mDebugProfiles, &CSMWorld::IdTable::dataChanged, this, &GlobalDebugProfileMenu::profileChanged); } void CSVDoc::GlobalDebugProfileMenu::updateActions(bool running) { if (mActions) mActions->setEnabled(!running); } void CSVDoc::GlobalDebugProfileMenu::profileAboutToBeRemoved(const QModelIndex& parent, int start, int end) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::profileInserted(const QModelIndex& parent, int start, int end) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::profileChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { rebuild(); } void CSVDoc::GlobalDebugProfileMenu::actionTriggered(QAction* action) { emit triggered(std::string(action->text().toUtf8().constData())); } openmw-openmw-0.49.0/apps/opencs/view/doc/globaldebugprofilemenu.hpp000066400000000000000000000017571503074453300256310ustar00rootroot00000000000000#ifndef CSV_DOC_GLOBALDEBUGPROFILEMENU_H #define CSV_DOC_GLOBALDEBUGPROFILEMENU_H #include #include class QAction; class QActionGroup; class QModelIndex; class QObject; class QWidget; namespace CSMWorld { class IdTable; } namespace CSVDoc { class GlobalDebugProfileMenu : public QMenu { Q_OBJECT CSMWorld::IdTable* mDebugProfiles; QActionGroup* mActions; private: void rebuild(); public: GlobalDebugProfileMenu(CSMWorld::IdTable* debugProfiles, QWidget* parent = nullptr); void updateActions(bool running); private slots: void profileAboutToBeRemoved(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 actionTriggered(QAction* action); signals: void triggered(const std::string& profile); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/loader.cpp000066400000000000000000000131251503074453300223450ustar00rootroot00000000000000#include "loader.hpp" #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) { event->ignore(); cancel(); } CSVDoc::LoadingDocument::LoadingDocument(CSMDoc::Document* document) : mDocument(document) , mTotalRecordsLabel(0) , mRecordsLabel(0) , mAborted(false) , mMessages(nullptr) , mRecords(0) { setWindowTitle("Opening " + Files::pathToQString(document->getSavePath().filename())); setMinimumWidth(400); mLayout = new QVBoxLayout(this); // total progress mTotalRecordsLabel = new QLabel(this); mLayout->addWidget(mTotalRecordsLabel); mTotalProgress = new QProgressBar(this); mLayout->addWidget(mTotalProgress); mTotalProgress->setMinimum(0); mTotalProgress->setMaximum(document->getData().getTotalRecords(document->getContentFiles())); mTotalProgress->setTextVisible(true); mTotalProgress->setValue(0); mTotalRecords = 0; mFilesLoaded = 0; // record progress mLayout->addWidget(mRecordsLabel = new QLabel("Records", this)); mRecordProgress = new QProgressBar(this); mLayout->addWidget(mRecordProgress); mRecordProgress->setMinimum(0); mRecordProgress->setTextVisible(true); mRecordProgress->setValue(0); // error message mError = new QLabel(this); mError->setWordWrap(true); mError->setTextInteractionFlags(Qt::TextSelectableByMouse); mLayout->addWidget(mError); // buttons mButtons = new QDialogButtonBox(QDialogButtonBox::Cancel, Qt::Horizontal, this); mLayout->addWidget(mButtons); setLayout(mLayout); move(QCursor::pos()); show(); connect(mButtons, &QDialogButtonBox::rejected, this, qOverload<>(&LoadingDocument::cancel)); } void CSVDoc::LoadingDocument::nextStage(const std::string& name, int fileRecords) { ++mFilesLoaded; size_t numFiles = mDocument->getContentFiles().size(); mTotalRecordsLabel->setText(QString::fromUtf8( ("Loading: " + name + " (" + std::to_string(mFilesLoaded) + " of " + std::to_string((numFiles)) + ")") .c_str())); mTotalRecords = mTotalProgress->value(); mRecordProgress->setValue(0); mRecordProgress->setMaximum(fileRecords > 0 ? fileRecords : 1); mRecords = fileRecords; } void CSVDoc::LoadingDocument::nextRecord(int records) { if (records <= mRecords) { mTotalProgress->setValue(mTotalRecords + records); mRecordProgress->setValue(records); mRecordsLabel->setText("Records: " + QString::number(records) + " of " + QString::number(mRecords)); } } void CSVDoc::LoadingDocument::abort(const std::string& error) { mAborted = true; 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) { if (!mMessages) { mMessages = new QListWidget(this); mLayout->insertWidget(4, mMessages); } new QListWidgetItem(QString::fromUtf8(message.c_str()), mMessages); } void CSVDoc::LoadingDocument::cancel() { if (!mAborted) emit cancel(mDocument); else { emit close(mDocument); deleteLater(); } } CSVDoc::Loader::~Loader() { for (std::map::iterator iter(mDocuments.begin()); iter != mDocuments.end(); ++iter) delete iter->second; } void CSVDoc::Loader::add(CSMDoc::Document* document) { LoadingDocument* loading = new LoadingDocument(document); mDocuments.insert(std::make_pair(document, loading)); 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) { std::map::iterator iter = mDocuments.begin(); for (; iter != mDocuments.end(); ++iter) if (iter->first == document) break; if (iter == mDocuments.end()) return; if (completed || error.empty()) { delete iter->second; mDocuments.erase(iter); } else { iter->second->abort(error); // Leave the window open for now (wait for the user to close it) mDocuments.erase(iter); } } void CSVDoc::Loader::nextStage(CSMDoc::Document* document, const std::string& name, int fileRecords) { std::map::iterator iter = mDocuments.find(document); if (iter != mDocuments.end()) iter->second->nextStage(name, fileRecords); } void CSVDoc::Loader::nextRecord(CSMDoc::Document* document, int records) { std::map::iterator iter = mDocuments.find(document); if (iter != mDocuments.end()) iter->second->nextRecord(records); } void CSVDoc::Loader::loadMessage(CSMDoc::Document* document, const std::string& message) { std::map::iterator iter = mDocuments.find(document); if (iter != mDocuments.end()) iter->second->addMessage(message); } openmw-openmw-0.49.0/apps/opencs/view/doc/loader.hpp000066400000000000000000000040531503074453300223520ustar00rootroot00000000000000#ifndef CSV_DOC_LOADER_H #define CSV_DOC_LOADER_H #include #include #include #include class QCloseEvent; class QLabel; class QProgressBar; class QDialogButtonBox; class QListWidget; class QVBoxLayout; namespace CSMDoc { class Document; } namespace CSVDoc { class LoadingDocument : public QWidget { Q_OBJECT 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: void closeEvent(QCloseEvent* event) override; public: LoadingDocument(CSMDoc::Document* document); void nextStage(const std::string& name, int totalRecords); void nextRecord(int records); void abort(const std::string& error); void addMessage(const std::string& message); private slots: void cancel(); signals: void cancel(CSMDoc::Document* document); ///< Stop loading process. void close(CSMDoc::Document* document); ///< Close stopped loading process. }; class Loader : public QObject { Q_OBJECT std::map mDocuments; public: Loader() = default; ~Loader() override; signals: void cancel(CSMDoc::Document* document); void close(CSMDoc::Document* document); public slots: void add(CSMDoc::Document* document); void loadingStopped(CSMDoc::Document* document, bool completed, const std::string& error); void nextStage(CSMDoc::Document* document, const std::string& name, int totalRecords); void nextRecord(CSMDoc::Document* document, int records); void loadMessage(CSMDoc::Document* document, const std::string& message); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/newgame.cpp000066400000000000000000000035611503074453300225250ustar00rootroot00000000000000#include "newgame.hpp" #include #include #include #include #include #include "adjusterwidget.hpp" #include "filewidget.hpp" CSVDoc::NewGameDialogue::NewGameDialogue() { setWindowTitle("Create New Game"); QVBoxLayout* layout = new QVBoxLayout(this); mFileWidget = new FileWidget(this); mFileWidget->setType(false); layout->addWidget(mFileWidget, 1); mAdjusterWidget = new AdjusterWidget(this); layout->addWidget(mAdjusterWidget, 1); QDialogButtonBox* buttons = new QDialogButtonBox(this); mCreate = new QPushButton("Create", this); mCreate->setDefault(true); mCreate->setEnabled(false); buttons->addButton(mCreate, QDialogButtonBox::AcceptRole); QPushButton* cancel = new QPushButton("Cancel", this); buttons->addButton(cancel, QDialogButtonBox::RejectRole); layout->addWidget(buttons); setLayout(layout); 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()); } void CSVDoc::NewGameDialogue::setLocalData(const std::filesystem::path& localData) { mAdjusterWidget->setLocalData(localData); } void CSVDoc::NewGameDialogue::stateChanged(bool valid) { mCreate->setEnabled(valid); } void CSVDoc::NewGameDialogue::create() { emit createRequest(mAdjusterWidget->getPath()); } void CSVDoc::NewGameDialogue::reject() { emit cancelCreateGame(); QDialog::reject(); } openmw-openmw-0.49.0/apps/opencs/view/doc/newgame.hpp000066400000000000000000000015421503074453300225270ustar00rootroot00000000000000#ifndef CSV_DOC_NEWGAME_H #define CSV_DOC_NEWGAME_H #include #include #include #ifndef CS_QT_STD_FILESYSTEM_PATH_DECLARED #define CS_QT_STD_FILESYSTEM_PATH_DECLARED Q_DECLARE_METATYPE(std::filesystem::path) #endif class QPushButton; namespace CSVDoc { class FileWidget; class AdjusterWidget; class NewGameDialogue : public QDialog { Q_OBJECT QPushButton* mCreate; FileWidget* mFileWidget; AdjusterWidget* mAdjusterWidget; public: NewGameDialogue(); void setLocalData(const std::filesystem::path& localData); signals: void createRequest(const std::filesystem::path& file); void cancelCreateGame(); private slots: void stateChanged(bool valid); void create(); void reject() override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/operation.cpp000066400000000000000000000075611503074453300231060ustar00rootroot00000000000000#include "operation.hpp" #include #include #include #include #include #include "../../model/doc/state.hpp" void CSVDoc::Operation::updateLabel(int threads) { if (threads == -1 || ((threads == 0) != mStalling)) { 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; } std::ostringstream stream; if ((mStalling = (threads <= 0))) { stream << name << " (waiting for a free worker thread)"; } else { stream << name << " (%p%)"; } mProgressBar->setFormat(stream.str().c_str()); } } 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); updateLabel(); /// \todo assign different progress bar colours to allow the user to distinguish easily between operation types } CSVDoc::Operation::~Operation() { delete mLayout; delete mProgressBar; delete mAbortButton; } void CSVDoc::Operation::initWidgets() { mProgressBar = new QProgressBar(); mAbortButton = new QPushButton("Abort"); mLayout = new QHBoxLayout(); mLayout->setContentsMargins(8, 4, 8, 4); mLayout->addWidget(mProgressBar); mLayout->addWidget(mAbortButton); connect(mAbortButton, &QPushButton::clicked, this, qOverload<>(&Operation::abortOperation)); } void CSVDoc::Operation::setProgress(int current, int max, int threads) { updateLabel(threads); mProgressBar->setRange(0, max); mProgressBar->setValue(current); } int CSVDoc::Operation::getType() const { return mType; } void CSVDoc::Operation::setBarColor(int type) { 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;" "}"; 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: topColor = "#f27d6e"; midTopColor = "#ee6954"; midBottomColor = "#f05536"; bottomColor = "#de511e"; // red break; case CSMDoc::State_Searching: topColor = "#6db3f2"; midTopColor = "#54a3ee"; midBottomColor = "#3690f0"; bottomColor = "#1e69de"; // blue break; case CSMDoc::State_Verifying: topColor = "#bfd255"; midTopColor = "#8eb92a"; midBottomColor = "#72aa00"; bottomColor = "#9ecb2d"; // green break; case CSMDoc::State_Merging: 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 { return mLayout; } void CSVDoc::Operation::abortOperation() { emit abortOperation(mType); } openmw-openmw-0.49.0/apps/opencs/view/doc/operation.hpp000066400000000000000000000016741503074453300231120ustar00rootroot00000000000000#ifndef CSV_DOC_OPERATION_H #define CSV_DOC_OPERATION_H #include class QHBoxLayout; class QPushButton; class QProgressBar; class QWidget; namespace CSVDoc { class Operation : public QObject { Q_OBJECT int mType; bool mStalling; QProgressBar* mProgressBar; QPushButton* mAbortButton; QHBoxLayout* mLayout; // not implemented Operation(const Operation&); Operation& operator=(const Operation&); void updateLabel(int threads = -1); public: Operation(int type, QWidget* parent); ~Operation() override; void setProgress(int current, int max, int threads); int getType() const; QHBoxLayout* getLayout() const; private: void setBarColor(int type); void initWidgets(); signals: void abortOperation(int type); private slots: void abortOperation(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/operations.cpp000066400000000000000000000037031503074453300232630ustar00rootroot00000000000000#include "operations.hpp" #include #include #include #include #include "operation.hpp" namespace { constexpr int operationLineHeight = 36; } CSVDoc::Operations::Operations() { setFeatures(QDockWidget::NoDockWidgetFeatures); QWidget* widgetContainer = new QWidget(this); mLayout = new QVBoxLayout; 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) { for (std::vector::iterator iter(mOperations.begin()); iter != mOperations.end(); ++iter) if ((*iter)->getType() == type) { (*iter)->setProgress(current, max, threads); return; } Operation* operation = new Operation(type, this); connect(operation, qOverload(&Operation::abortOperation), this, &Operations::abortOperation); mLayout->addLayout(operation->getLayout()); mOperations.push_back(operation); operation->setProgress(current, max, threads); int newCount = static_cast(mOperations.size()); setFixedHeight(operationLineHeight * newCount); setVisible(true); } void CSVDoc::Operations::quitOperation(int type) { for (std::vector::iterator iter(mOperations.begin()); iter != mOperations.end(); ++iter) if ((*iter)->getType() == type) { mLayout->removeItem((*iter)->getLayout()); (*iter)->deleteLater(); mOperations.erase(iter); int newCount = static_cast(mOperations.size()); if (newCount > 0) setFixedHeight(operationLineHeight * newCount); else setVisible(false); break; } } openmw-openmw-0.49.0/apps/opencs/view/doc/operations.hpp000066400000000000000000000014361503074453300232710ustar00rootroot00000000000000#ifndef CSV_DOC_OPERATIONS_H #define CSV_DOC_OPERATIONS_H #include #include class QVBoxLayout; namespace CSVDoc { class Operation; class Operations : public QDockWidget { Q_OBJECT QVBoxLayout* mLayout; std::vector mOperations; // not implemented Operations(const Operations&); Operations& operator=(const Operations&); public: Operations(); 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. signals: void abortOperation(int type); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/runlogsubview.cpp000066400000000000000000000007771503074453300240230ustar00rootroot00000000000000#include "runlogsubview.hpp" #include "../../model/doc/document.hpp" #include #include CSVDoc::RunLogSubView::RunLogSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) : SubView(id) { QTextEdit* edit = new QTextEdit(this); edit->setDocument(document.getRunLog()); edit->setReadOnly(true); setWidget(edit); } void CSVDoc::RunLogSubView::setEditLock(bool locked) { // ignored since this SubView does not have editing } openmw-openmw-0.49.0/apps/opencs/view/doc/runlogsubview.hpp000066400000000000000000000006551503074453300240230ustar00rootroot00000000000000#ifndef CSV_DOC_RUNLOGSUBVIEW_H #define CSV_DOC_RUNLOGSUBVIEW_H #include "subview.hpp" #include namespace CSMDoc { class Document; } namespace CSVDoc { class RunLogSubView : public SubView { Q_OBJECT public: RunLogSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/sizehint.cpp000066400000000000000000000004041503074453300227300ustar00rootroot00000000000000#include "sizehint.hpp" CSVDoc::SizeHintWidget::SizeHintWidget(QWidget* parent) : QWidget(parent) { } QSize CSVDoc::SizeHintWidget::sizeHint() const { return mSize; } void CSVDoc::SizeHintWidget::setSizeHint(const QSize& size) { mSize = size; } openmw-openmw-0.49.0/apps/opencs/view/doc/sizehint.hpp000066400000000000000000000006401503074453300227370ustar00rootroot00000000000000#ifndef CSV_DOC_SIZEHINT_H #define CSV_DOC_SIZEHINT_H #include #include namespace CSVDoc { class SizeHintWidget : public QWidget { QSize mSize; public: SizeHintWidget(QWidget* parent = nullptr); ~SizeHintWidget() override = default; QSize sizeHint() const override; void setSizeHint(const QSize& size); }; } #endif // CSV_DOC_SIZEHINT_H openmw-openmw-0.49.0/apps/opencs/view/doc/startup.cpp000066400000000000000000000074451503074453300226110ustar00rootroot00000000000000#include "startup.hpp" #include #include #include #include #include #include #include #include #include #include QPushButton* CSVDoc::StartupDialogue::addButton(const QString& label, const QString& icon) { int column = mColumn--; QPushButton* button = new QPushButton(this); button->setIcon(Misc::ScalableIcon::load(icon)); button->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred)); mLayout->addWidget(button, 0, column); mLayout->addWidget(new QLabel(label, this), 1, column, Qt::AlignCenter); int width = mLayout->itemAtPosition(1, column)->widget()->sizeHint().width(); if (width > mWidth) mWidth = width; return button; } QWidget* CSVDoc::StartupDialogue::createButtons() { QWidget* widget = new QWidget(this); mLayout = new QGridLayout(widget); /// \todo add icons QPushButton* loadDocument = addButton("Edit A Content File", ":startup/edit-content"); connect(loadDocument, &QPushButton::clicked, this, &StartupDialogue::loadDocument); QPushButton* createAddon = addButton("Create A New Addon", ":startup/create-addon"); connect(createAddon, &QPushButton::clicked, this, &StartupDialogue::createAddon); 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); mLayout->setRowMinimumHeight(0, mWidth); mLayout->setSizeConstraint(QLayout::SetMinimumSize); mLayout->setHorizontalSpacing(32); mLayout->setContentsMargins(16, 16, 16, 8); loadDocument->setIconSize(QSize(mWidth, mWidth)); createGame->setIconSize(QSize(mWidth, mWidth)); createAddon->setIconSize(QSize(mWidth, mWidth)); widget->setLayout(mLayout); return widget; } QWidget* CSVDoc::StartupDialogue::createTools() { QWidget* widget = new QWidget(this); QHBoxLayout* layout = new QHBoxLayout(widget); layout->setDirection(QBoxLayout::RightToLeft); layout->setContentsMargins(4, 4, 4, 4); QPushButton* config = new QPushButton(widget); config->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); config->setIcon(Misc::ScalableIcon::load(":startup/configure")); config->setToolTip("Open user settings"); layout->addWidget(config); layout->addWidget(new QWidget, 1); // dummy widget; stops buttons from taking all the space widget->setLayout(layout); connect(config, &QPushButton::clicked, this, &StartupDialogue::editConfig); return widget; } CSVDoc::StartupDialogue::StartupDialogue() : mWidth(0) , mColumn(2) { setWindowTitle("OpenMW-CS"); QVBoxLayout* layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); 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.
"); QFont font; font.setPointSize(12); font.setBold(true); warning->setFont(font); warning->setWordWrap(true); layout->addWidget(warning, 1); setLayout(layout); QRect scr = QGuiApplication::primaryScreen()->geometry(); QRect rect = geometry(); move(scr.center().x() - rect.center().x(), scr.center().y() - rect.center().y()); } openmw-openmw-0.49.0/apps/opencs/view/doc/startup.hpp000066400000000000000000000012171503074453300226050ustar00rootroot00000000000000#ifndef CSV_DOC_STARTUP_H #define CSV_DOC_STARTUP_H #include class QGridLayout; class QString; class QPushButton; class QWidget; class QIcon; namespace CSVDoc { class StartupDialogue : public QWidget { Q_OBJECT private: int mWidth; int mColumn; QGridLayout* mLayout; QPushButton* addButton(const QString& label, const QString& icon); QWidget* createButtons(); QWidget* createTools(); public: StartupDialogue(); signals: void createGame(); void createAddon(); void loadDocument(); void editConfig(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/subview.cpp000066400000000000000000000026271503074453300225700ustar00rootroot00000000000000#include "subview.hpp" #include #include #include bool CSVDoc::SubView::event(QEvent* event) { if (event->type() == QEvent::ShortcutOverride) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_W && keyEvent->modifiers() == (Qt::ShiftModifier | Qt::ControlModifier)) emit closeRequest(); return true; } return QDockWidget::event(event); } 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())); setAttribute(Qt::WA_DeleteOnClose); } CSMWorld::UniversalId CSVDoc::SubView::getUniversalId() const { return mUniversalId; } void CSVDoc::SubView::setStatusBar(bool show) {} void CSVDoc::SubView::useHint(const std::string& hint) {} void CSVDoc::SubView::setUniversalId(const CSMWorld::UniversalId& id) { mUniversalId = id; setWindowTitle(QString::fromUtf8(mUniversalId.toString().c_str())); emit universalIdChanged(mUniversalId); } void CSVDoc::SubView::closeEvent(QCloseEvent* event) { emit updateSubViewIndices(this); } std::string CSVDoc::SubView::getTitle() const { return mUniversalId.toString(); } void CSVDoc::SubView::closeRequest() { emit closeRequest(this); } openmw-openmw-0.49.0/apps/opencs/view/doc/subview.hpp000066400000000000000000000025721503074453300225740ustar00rootroot00000000000000#ifndef CSV_DOC_SUBVIEW_H #define CSV_DOC_SUBVIEW_H #include "../../model/world/universalid.hpp" #include #include class QCloseEvent; class QEvent; class QObject; namespace CSVDoc { class SubView : public QDockWidget { Q_OBJECT CSMWorld::UniversalId mUniversalId; // not implemented SubView(const SubView&); SubView& operator=(SubView&); protected: void setUniversalId(const CSMWorld::UniversalId& id); bool event(QEvent* event) override; public: SubView(const CSMWorld::UniversalId& id); CSMWorld::UniversalId getUniversalId() const; virtual void setEditLock(bool locked) = 0; virtual void setStatusBar(bool show); ///< Default implementation: ignored virtual void useHint(const std::string& hint); ///< Default implementation: ignored virtual std::string getTitle() const; private: void closeEvent(QCloseEvent* event) override; signals: void focusId(const CSMWorld::UniversalId& universalId, const std::string& hint); void closeRequest(SubView* subView); void updateTitle(); void updateSubViewIndices(SubView* view = nullptr); void universalIdChanged(const CSMWorld::UniversalId& universalId); protected slots: void closeRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/subviewfactory.cpp000066400000000000000000000021141503074453300241470ustar00rootroot00000000000000#include "subviewfactory.hpp" #include #include #include #include #include #include CSVDoc::SubViewFactoryManager::~SubViewFactoryManager() { 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) { assert(mSubViewFactories.find(id) == mSubViewFactories.end()); mSubViewFactories.insert(std::make_pair(id, factory)); } CSVDoc::SubView* CSVDoc::SubViewFactoryManager::makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) { 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()); return iter->second->makeSubView(id, document); } openmw-openmw-0.49.0/apps/opencs/view/doc/subviewfactory.hpp000066400000000000000000000025271503074453300241640ustar00rootroot00000000000000#ifndef CSV_DOC_SUBVIEWFACTORY_H #define CSV_DOC_SUBVIEWFACTORY_H #include #include "../../model/world/universalid.hpp" namespace CSMDoc { class Document; } namespace CSVDoc { class SubView; class SubViewFactoryBase { 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; 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. SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); ///< The ownership of the returned sub view is not transferred. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/subviewfactoryimp.hpp000066400000000000000000000026071503074453300246710ustar00rootroot00000000000000#ifndef CSV_DOC_SUBVIEWFACTORYIMP_H #define CSV_DOC_SUBVIEWFACTORYIMP_H #include "../../model/doc/document.hpp" #include "subviewfactory.hpp" namespace CSVDoc { template class SubViewFactory : public SubViewFactoryBase { public: CSVDoc::SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; template CSVDoc::SubView* SubViewFactory::makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) { return new SubViewT(id, document); } template class SubViewFactoryWithCreator : public SubViewFactoryBase { bool mSorting; public: SubViewFactoryWithCreator(bool sorting = true); CSVDoc::SubView* makeSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) override; }; template SubViewFactoryWithCreator::SubViewFactoryWithCreator(bool sorting) : mSorting(sorting) { } template CSVDoc::SubView* SubViewFactoryWithCreator::makeSubView( const CSMWorld::UniversalId& id, CSMDoc::Document& document) { return new SubViewT(id, document, CreatorFactoryT(), mSorting); } } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/view.cpp000066400000000000000000001115011503074453300220460ustar00rootroot00000000000000#include "view.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/doc/state.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/idtable.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 #include "globaldebugprofilemenu.hpp" #include "operations.hpp" #include "runlogsubview.hpp" #include "subview.hpp" #include "subviewfactoryimp.hpp" #include "viewmanager.hpp" QRect desktopRect() { 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 { // closeRequest() returns true if last document mViewManager.removeDocAndView(mDocument); } } void CSVDoc::View::setupFileMenu() { QMenu* file = menuBar()->addMenu(tr("File")); 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", file, "document-file-newaddon"); connect(newAddon, &QAction::triggered, this, &View::newAddonRequest); QAction* open = createMenuEntry("Open", ":menu-open", file, "document-file-open"); connect(open, &QAction::triggered, this, &View::loadDocumentRequest); QAction* save = createMenuEntry("Save", ":menu-save", file, "document-file-save"); connect(save, &QAction::triggered, this, &View::save); mSave = save; 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", file, "document-file-merge"); connect(merge, &QAction::triggered, this, &View::merge); mMerge = merge; 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, &QAction::triggered, this, &View::addMetaDataSubView); file->addSeparator(); QAction* close = createMenuEntry("Close", ":menu-close", file, "document-file-close"); connect(close, &QAction::triggered, this, &View::close); QAction* exit = createMenuEntry("Exit", ":menu-exit", file, "document-file-exit"); connect(exit, &QAction::triggered, this, &View::exit); connect(this, &View::exitApplicationRequest, &mViewManager, &ViewManager::exitApplication); } namespace { void updateUndoRedoAction(QAction* action, const std::string& settingsKey) { QKeySequence seq; CSMPrefs::State::get().getShortcutManager().getSequence(settingsKey, seq); action->setShortcut(seq); } } void CSVDoc::View::undoActionChanged() { updateUndoRedoAction(mUndo, "document-edit-undo"); } void CSVDoc::View::redoActionChanged() { updateUndoRedoAction(mRedo, "document-edit-redo"); } void CSVDoc::View::setupEditMenu() { QMenu* edit = menuBar()->addMenu(tr("Edit")); mUndo = mDocument->getUndoStack().createUndoAction(this, tr("Undo")); setupShortcut("document-edit-undo", 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, &QAction::changed, this, &View::redoActionChanged); setupShortcut("document-edit-redo", mRedo); mRedo->setIcon(Misc::ScalableIcon::load(":menu-redo")); edit->addAction(mRedo); 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, &QAction::triggered, this, &View::addSearchSubView); } void CSVDoc::View::setupViewMenu() { QMenu* view = menuBar()->addMenu(tr("View")); 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", 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); 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")); QAction* referenceables = createMenuEntry(CSMWorld::UniversalId::Type_Referenceables, world, "document-world-referencables"); connect(referenceables, &QAction::triggered, this, &View::addReferenceablesSubView); QAction* references = createMenuEntry(CSMWorld::UniversalId::Type_References, world, "document-world-references"); connect(references, &QAction::triggered, this, &View::addReferencesSubView); world->addSeparator(); 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* landTextures = createMenuEntry(CSMWorld::UniversalId::Type_LandTextures, world, "document-world-landtextures"); connect(landTextures, &QAction::triggered, this, &View::addLandTexturesSubView); QAction* grid = createMenuEntry(CSMWorld::UniversalId::Type_Pathgrids, world, "document-world-pathgrid"); connect(grid, &QAction::triggered, this, &View::addPathgridSubView); world->addSeparator(); 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, &QAction::triggered, this, &View::addRegionMapSubView); } void CSVDoc::View::setupMechanicsMenu() { 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, &QAction::triggered, this, &View::addGlobalsSubView); QAction* gmsts = createMenuEntry(CSMWorld::UniversalId::Type_Gmsts, mechanics, "document-mechanics-gamesettings"); connect(gmsts, &QAction::triggered, this, &View::addGmstsSubView); mechanics->addSeparator(); QAction* spells = createMenuEntry(CSMWorld::UniversalId::Type_Spells, mechanics, "document-mechanics-spells"); connect(spells, &QAction::triggered, this, &View::addSpellsSubView); 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, &QAction::triggered, this, &View::addMagicEffectsSubView); } void CSVDoc::View::setupCharacterMenu() { QMenu* characters = menuBar()->addMenu(tr("Characters")); QAction* skills = createMenuEntry(CSMWorld::UniversalId::Type_Skills, characters, "document-character-skills"); connect(skills, &QAction::triggered, this, &View::addSkillsSubView); QAction* classes = createMenuEntry(CSMWorld::UniversalId::Type_Classes, characters, "document-character-classes"); connect(classes, &QAction::triggered, this, &View::addClassesSubView); QAction* factions = createMenuEntry(CSMWorld::UniversalId::Type_Faction, characters, "document-character-factions"); connect(factions, &QAction::triggered, this, &View::addFactionsSubView); QAction* races = createMenuEntry(CSMWorld::UniversalId::Type_Races, characters, "document-character-races"); 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); characters->addSeparator(); QAction* topics = createMenuEntry(CSMWorld::UniversalId::Type_Topics, characters, "document-character-topics"); connect(topics, &QAction::triggered, this, &View::addTopicsSubView); QAction* topicInfos = createMenuEntry(CSMWorld::UniversalId::Type_TopicInfos, characters, "document-character-topicinfos"); connect(topicInfos, &QAction::triggered, this, &View::addTopicInfosSubView); characters->addSeparator(); QAction* journals = createMenuEntry(CSMWorld::UniversalId::Type_Journals, characters, "document-character-journals"); connect(journals, &QAction::triggered, this, &View::addJournalsSubView); 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")); 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, &QAction::triggered, this, &View::addSoundsSubView); QAction* soundGens = createMenuEntry(CSMWorld::UniversalId::Type_SoundGens, assets, "document-assets-soundgens"); 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, &QAction::triggered, this, &View::addMeshesSubView); QAction* icons = createMenuEntry(CSMWorld::UniversalId::Type_Icons, assets, "document-assets-icons"); connect(icons, &QAction::triggered, this, &View::addIconsSubView); QAction* musics = createMenuEntry(CSMWorld::UniversalId::Type_Musics, assets, "document-assets-musics"); connect(musics, &QAction::triggered, this, &View::addMusicsSubView); QAction* soundFiles = createMenuEntry(CSMWorld::UniversalId::Type_SoundsRes, assets, "document-assets-soundres"); connect(soundFiles, &QAction::triggered, this, &View::addSoundsResSubView); QAction* textures = createMenuEntry(CSMWorld::UniversalId::Type_Textures, assets, "document-assets-textures"); connect(textures, &QAction::triggered, this, &View::addTexturesSubView); QAction* videos = createMenuEntry(CSMWorld::UniversalId::Type_Videos, assets, "document-assets-videos"); connect(videos, &QAction::triggered, this, &View::addVideosSubView); } void CSVDoc::View::setupDebugMenu() { QMenu* debug = menuBar()->addMenu(tr("Debug")); QAction* profiles = createMenuEntry(CSMWorld::UniversalId::Type_DebugProfiles, debug, "document-debug-profiles"); connect(profiles, &QAction::triggered, this, &View::addDebugProfilesSubView); debug->addSeparator(); mGlobalDebugProfileMenu = new GlobalDebugProfileMenu( &dynamic_cast( *mDocument->getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)), this); connect(mGlobalDebugProfileMenu, &GlobalDebugProfileMenu::triggered, this, [this](const std::string& profile) { this->run(profile, ""); }); QAction* runDebug = debug->addMenu(mGlobalDebugProfileMenu); runDebug->setText(tr("Run OpenMW")); setupShortcut("document-debug-run", runDebug); runDebug->setIcon(Misc::ScalableIcon::load(":run-openmw")); 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, &QAction::triggered, this, &View::addRunLogSubView); } void CSVDoc::View::setupHelpMenu() { QMenu* help = menuBar()->addMenu(tr("Help")); QAction* helpInfo = createMenuEntry("Help", ":info", help, "document-help-help"); connect(helpInfo, &QAction::triggered, this, &View::openHelp); QAction* tutorial = createMenuEntry("Tutorial", ":info", help, "document-help-tutorial"); connect(tutorial, &QAction::triggered, this, &View::tutorial); QAction* about = createMenuEntry("About OpenMW-CS", ":info", help, "document-help-about"); connect(about, &QAction::triggered, this, &View::infoAbout); 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); setupShortcut(shortcutName, entry); const std::string iconName = CSMWorld::UniversalId(type).getIcon(); if (!iconName.empty() && iconName != ":placeholder") entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); menu->addAction(entry); return entry; } 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); setupShortcut(shortcutName, entry); if (!iconName.empty() && iconName != ":placeholder") entry->setIcon(Misc::ScalableIcon::load(QString::fromStdString(iconName))); menu->addAction(entry); return entry; } void CSVDoc::View::setupUi() { setupFileMenu(); setupEditMenu(); setupViewMenu(); setupWorldMenu(); setupMechanicsMenu(); setupCharacterMenu(); setupAssetsMenu(); setupDebugMenu(); setupHelpMenu(); } void CSVDoc::View::setupShortcut(const char* name, QAction* action) { CSMPrefs::Shortcut* shortcut = new CSMPrefs::Shortcut(name, this); shortcut->associateAction(action); } void CSVDoc::View::updateTitle() { std::ostringstream stream; stream << Files::pathToUnicodeString(mDocument->getSavePath().filename()); if (mDocument->getState() & CSMDoc::State_Modified) stream << " *"; 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(); if (hideTitle) stream << " - " << mSubViews.at(0)->getTitle(); setWindowTitle(QString::fromUtf8(stream.str().c_str())); } void CSVDoc::View::updateSubViewIndices(SubView* view) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; if (view && mSubViews.contains(view)) { mSubViews.removeOne(view); // adjust (reduce) the scroll area (even floating), except when it is "Scrollbar Only" if (windows["mainwindow-scrollbar"].toString() == "Grow then Scroll") updateScrollbar(); } bool hideTitle = windows["hide-subview"].isTrue() && mSubViews.size() == 1 && !mSubViews.at(0)->isFloating(); updateTitle(); for (SubView* subView : mSubViews) { if (!subView->isFloating()) { if (hideTitle) { subView->setTitleBarWidget(new QWidget(this)); subView->setWindowTitle(QString::fromUtf8(subView->getTitle().c_str())); } else { delete subView->titleBarWidget(); subView->setTitleBarWidget(nullptr); } } } } 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); 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)); mGlobalDebugProfileMenu->updateActions(running); mStopDebug->setEnabled(running); 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) { 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); resize(width, height); mSubViewWindow.setDockOptions(QMainWindow::AllowNestedDocks); if (windows["mainwindow-scrollbar"].toString() == "Grow Only") { setCentralWidget(&mSubViewWindow); } else { createScrollArea(); } mOperations = new Operations; addDockWidget(Qt::BottomDockWidgetArea, mOperations); setContextMenuPolicy(Qt::NoContextMenu); updateTitle(); setupUi(); updateActions(); CSVWorld::addSubViewFactories(mSubViewFactory); CSVTools::addSubViewFactories(mSubViewFactory); mSubViewFactory.add(CSMWorld::UniversalId::Type_RunLog, new SubViewFactory); connect(mOperations, &Operations::abortOperation, this, &View::abortOperation); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &View::settingChanged); } const CSMDoc::Document* CSVDoc::View::getDocument() const { return mDocument; } CSMDoc::Document* CSVDoc::View::getDocument() { return mDocument; } void CSVDoc::View::setIndex(int viewIndex, int totalViews) { mViewIndex = viewIndex; mViewTotal = totalViews; updateTitle(); } void CSVDoc::View::updateDocumentState() { updateTitle(); updateActions(); static const int operations[] = { CSMDoc::State_Saving, CSMDoc::State_Verifying, CSMDoc::State_Searching, CSMDoc::State_Merging, // end marker -1, }; int state = mDocument->getState(); for (int i = 0; operations[i] != -1; ++i) if (!(state & operations[i])) mOperations->quitOperation(operations[i]); QList subViews = findChildren(); 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) { mOperations->setProgress(current, max, type, threads); } void CSVDoc::View::addSubView(const CSMWorld::UniversalId& id, const std::string& hint) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; bool isReferenceable = id.getClass() == CSMWorld::UniversalId::Class_RefRecord; // User setting to reuse sub views (on a per top level view basis) if (windows["reuse"].isTrue()) { for (SubView* sb : mSubViews) { bool isSubViewReferenceable = sb->getUniversalId().getType() == CSMWorld::UniversalId::Type_Referenceable; if ((isReferenceable && isSubViewReferenceable && id.getId() == sb->getUniversalId().getId()) || (!isReferenceable && id == sb->getUniversalId())) { sb->setFocus(); if (!hint.empty()) sb->useHint(hint); return; } } } if (mScroll) 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 { mViewManager.addView(mDocument, id, hint); return; } SubView* view = nullptr; if (isReferenceable) { view = mSubViewFactory.makeSubView( CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Referenceable, id), *mDocument); } else { view = mSubViewFactory.makeSubView(id, *mDocument); } assert(view); view->setParent(this); view->setEditLock(mDocument->getState() & CSMDoc::State_Locked); mSubViews.append(view); // only after assert int minWidth = windows["minimum-width"].toInt(); view->setMinimumWidth(minWidth); view->setStatusBar(mShowStatusBar->isChecked()); // Work out how to deal with additional subviews // // Policy for "Grow then Scroll": // // - Increase the horizontal width of the mainwindow until it becomes greater than or equal // to the screen (monitor) width. // - Move the mainwindow position sideways if necessary to fit within the screen. // - Any more additions increases the size of the mSubViewWindow (horizontal scrollbar // should become visible) // - Move the scroll bar to the newly added subview // 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); updateSubViewIndices(); connect(view, &SubView::focusId, this, &View::addSubView); connect(view, qOverload(&SubView::closeRequest), this, &View::closeRequest); connect(view, &SubView::updateTitle, this, &View::updateTitle); connect(view, &SubView::updateSubViewIndices, this, &View::updateSubViewIndices); CSVWorld::TableSubView* tableView = dynamic_cast(view); if (tableView) { connect(this, &View::requestFocus, tableView, &CSVWorld::TableSubView::requestFocus); } CSVWorld::SceneSubView* sceneView = dynamic_cast(view); if (sceneView) { 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); } void CSVDoc::View::moveScrollBarToEnd(int min, int max) { if (mScroll) { mScroll->horizontalScrollBar()->setValue(max); QObject::disconnect(mScroll->horizontalScrollBar(), &QScrollBar::rangeChanged, this, &View::moveScrollBarToEnd); } } void CSVDoc::View::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "Windows/hide-subview") updateSubViewIndices(nullptr); else if (*setting == "Windows/mainwindow-scrollbar") { if (setting->toString() != "Grow Only") { if (mScroll) { if (setting->toString() == "Scrollbar Only") { mScrollbarOnly = true; mSubViewWindow.setMinimumWidth(0); } else if (mScrollbarOnly) { mScrollbarOnly = false; updateScrollbar(); } } else { createScrollArea(); } } else if (mScroll) { mScroll->takeWidget(); setCentralWidget(&mSubViewWindow); mScroll->deleteLater(); mScroll = nullptr; } } } void CSVDoc::View::newView() { mViewManager.addView(mDocument); } void CSVDoc::View::save() { mDocument->save(); } void CSVDoc::View::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/index.html"); } void CSVDoc::View::tutorial() { Misc::HelpViewer::openHelp("manuals/openmw-cs/tour.html"); } void CSVDoc::View::infoAbout() { // Get current OpenMW version QString versionInfo = (Version::getOpenmwVersionDescription() + #if defined(__x86_64__) || defined(_M_X64) " (64-bit)") .c_str(); #else " (32-bit)") .c_str(); #endif // Get current year 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.c_str()), tr("Home Page:"), tr("Forum:"), tr("Bug Tracker:"), tr("IRC:")); QMessageBox::about(this, "About OpenMW-CS", aboutText); } void CSVDoc::View::infoAboutQt() { QMessageBox::aboutQt(this); } void CSVDoc::View::verify() { addSubView(mDocument->verify()); } void CSVDoc::View::addGlobalsSubView() { addSubView(CSMWorld::UniversalId::Type_Globals); } void CSVDoc::View::addGmstsSubView() { addSubView(CSMWorld::UniversalId::Type_Gmsts); } void CSVDoc::View::addSkillsSubView() { addSubView(CSMWorld::UniversalId::Type_Skills); } void CSVDoc::View::addClassesSubView() { addSubView(CSMWorld::UniversalId::Type_Classes); } void CSVDoc::View::addFactionsSubView() { addSubView(CSMWorld::UniversalId::Type_Factions); } void CSVDoc::View::addRacesSubView() { addSubView(CSMWorld::UniversalId::Type_Races); } void CSVDoc::View::addSoundsSubView() { addSubView(CSMWorld::UniversalId::Type_Sounds); } void CSVDoc::View::addScriptsSubView() { addSubView(CSMWorld::UniversalId::Type_Scripts); } void CSVDoc::View::addRegionsSubView() { addSubView(CSMWorld::UniversalId::Type_Regions); } void CSVDoc::View::addBirthsignsSubView() { addSubView(CSMWorld::UniversalId::Type_Birthsigns); } void CSVDoc::View::addSpellsSubView() { addSubView(CSMWorld::UniversalId::Type_Spells); } void CSVDoc::View::addCellsSubView() { addSubView(CSMWorld::UniversalId::Type_Cells); } void CSVDoc::View::addReferenceablesSubView() { addSubView(CSMWorld::UniversalId::Type_Referenceables); } void CSVDoc::View::addReferencesSubView() { addSubView(CSMWorld::UniversalId::Type_References); } void CSVDoc::View::addRegionMapSubView() { addSubView(CSMWorld::UniversalId::Type_RegionMap); } void CSVDoc::View::addFiltersSubView() { addSubView(CSMWorld::UniversalId::Type_Filters); } void CSVDoc::View::addTopicsSubView() { addSubView(CSMWorld::UniversalId::Type_Topics); } void CSVDoc::View::addJournalsSubView() { addSubView(CSMWorld::UniversalId::Type_Journals); } void CSVDoc::View::addTopicInfosSubView() { addSubView(CSMWorld::UniversalId::Type_TopicInfos); } void CSVDoc::View::addJournalInfosSubView() { addSubView(CSMWorld::UniversalId::Type_JournalInfos); } void CSVDoc::View::addEnchantmentsSubView() { addSubView(CSMWorld::UniversalId::Type_Enchantments); } void CSVDoc::View::addBodyPartsSubView() { addSubView(CSMWorld::UniversalId::Type_BodyParts); } void CSVDoc::View::addSoundGensSubView() { addSubView(CSMWorld::UniversalId::Type_SoundGens); } void CSVDoc::View::addMeshesSubView() { addSubView(CSMWorld::UniversalId::Type_Meshes); } void CSVDoc::View::addIconsSubView() { addSubView(CSMWorld::UniversalId::Type_Icons); } void CSVDoc::View::addMusicsSubView() { addSubView(CSMWorld::UniversalId::Type_Musics); } void CSVDoc::View::addSoundsResSubView() { addSubView(CSMWorld::UniversalId::Type_SoundsRes); } void CSVDoc::View::addMagicEffectsSubView() { addSubView(CSMWorld::UniversalId::Type_MagicEffects); } void CSVDoc::View::addTexturesSubView() { addSubView(CSMWorld::UniversalId::Type_Textures); } void CSVDoc::View::addVideosSubView() { addSubView(CSMWorld::UniversalId::Type_Videos); } void CSVDoc::View::addDebugProfilesSubView() { addSubView(CSMWorld::UniversalId::Type_DebugProfiles); } void CSVDoc::View::addRunLogSubView() { addSubView(CSMWorld::UniversalId::Type_RunLog); } void CSVDoc::View::addLandsSubView() { addSubView(CSMWorld::UniversalId::Type_Lands); } void CSVDoc::View::addLandTexturesSubView() { addSubView(CSMWorld::UniversalId::Type_LandTextures); } void CSVDoc::View::addPathgridSubView() { addSubView(CSMWorld::UniversalId::Type_Pathgrids); } void CSVDoc::View::addStartScriptsSubView() { addSubView(CSMWorld::UniversalId::Type_StartScripts); } void CSVDoc::View::addSearchSubView() { addSubView(mDocument->newSearch()); } void CSVDoc::View::addMetaDataSubView() { addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_MetaData, "sys::meta")); } void CSVDoc::View::abortOperation(int type) { mDocument->abortOperation(type); updateActions(); } CSVDoc::Operations* CSVDoc::View::getOperations() const { return mOperations; } void CSVDoc::View::exit() { emit exitApplicationRequest(this); } void CSVDoc::View::resizeViewWidth(int width) { if (width >= 0) resize(width, geometry().height()); } void CSVDoc::View::resizeViewHeight(int height) { if (height >= 0) resize(geometry().width(), height); } void CSVDoc::View::toggleShowStatusBar(bool show) { for (QObject* view : mSubViewWindow.children()) { if (CSVDoc::SubView* subView = dynamic_cast(view)) subView->setStatusBar(show); } } void CSVDoc::View::toggleStatusBar(bool checked) { mShowStatusBar->setChecked(checked); } void CSVDoc::View::loadErrorLog() { addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_LoadErrorLog, 0)); } void CSVDoc::View::run(const std::string& profile, const std::string& startupInstruction) { mDocument->startRunning(profile, startupInstruction); } void CSVDoc::View::stop() { mDocument->stopRunning(); } void CSVDoc::View::closeRequest(SubView* subView) { CSMPrefs::Category& windows = CSMPrefs::State::get()["Windows"]; if (mSubViews.size() > 1 || mViewTotal <= 1 || !windows["hide-subview"].isTrue()) { subView->deleteLater(); mSubViews.removeOne(subView); } else if (mViewManager.closeRequest(this)) mViewManager.removeDocAndView(mDocument); } void CSVDoc::View::updateScrollbar() { QRect rect; QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) rect = topLevel->rect(); else rect = this->rect(); int newWidth = 0; for (int i = 0; i < mSubViews.size(); ++i) { newWidth += mSubViews[i]->width(); } int frameWidth = frameGeometry().width() - width(); if ((newWidth + frameWidth) >= rect.width()) mSubViewWindow.setMinimumWidth(newWidth); else mSubViewWindow.setMinimumWidth(0); } void CSVDoc::View::merge() { emit mergeDocument(mDocument); } void CSVDoc::View::updateWidth(bool isGrowLimit, int minSubViewWidth) { QRect rect; if (isGrowLimit) { QScreen* screen = getWidgetScreen(pos()); rect = screen->geometry(); } else rect = desktopRect(); if (!mScrollbarOnly && mScroll && mSubViews.size() > 1) { int newWidth = width() + minSubViewWidth; int frameWidth = frameGeometry().width() - 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 } else { // full width resize(rect.width() - frameWidth, height()); mSubViewWindow.setMinimumWidth(mSubViewWindow.width() + minSubViewWidth); move(0, y()); } } } void CSVDoc::View::createScrollArea() { mScroll = new QScrollArea(this); mScroll->setWidgetResizable(true); mScroll->setWidget(&mSubViewWindow); setCentralWidget(mScroll); } void CSVDoc::View::onRequestFocus(const std::string& id) { if (CSMPrefs::get()["3D Scene Editing"]["open-list-view"].isTrue()) { addReferencesSubView(); emit requestFocus(id); } else { addSubView(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Reference, id)); } } QScreen* CSVDoc::View::getWidgetScreen(const QPoint& position) { QScreen* screen = QApplication::screenAt(position); if (screen) return screen; const QList screens = QApplication::screens(); if (screens.isEmpty()) throw std::runtime_error("No screens available"); int closestDistance = std::numeric_limits::max(); for (QScreen* candidate : screens) { const QRect geometry = candidate->geometry(); const int dx = position.x() - std::clamp(position.x(), geometry.left(), geometry.right()); const int dy = position.y() - std::clamp(position.y(), geometry.top(), geometry.bottom()); const int distance = dx * dx + dy * dy; if (distance < closestDistance) { closestDistance = distance; screen = candidate; } } if (screen == nullptr) screen = screens.first(); return screen; } openmw-openmw-0.49.0/apps/opencs/view/doc/view.hpp000066400000000000000000000132261503074453300220600ustar00rootroot00000000000000#ifndef CSV_DOC_VIEW_H #define CSV_DOC_VIEW_H #include #include #include #include #include #include "subviewfactory.hpp" class QAction; class QCloseEvent; class QMenu; class QObject; class QScrollArea; namespace CSMDoc { class Document; } namespace CSMPrefs { class Setting; } 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; private: void closeEvent(QCloseEvent* event) override; 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 setupFileMenu(); void setupEditMenu(); void setupViewMenu(); void setupWorldMenu(); void setupMechanicsMenu(); void setupCharacterMenu(); void setupAssetsMenu(); void setupDebugMenu(); void setupHelpMenu(); void setupUi(); void setupShortcut(const char* name, QAction* action); void updateActions(); void exitApplication(); /// User preference function void resizeViewWidth(int width); /// User preference function void resizeViewHeight(int height); void updateScrollbar(); void updateWidth(bool isGrowLimit, int minSubViewWidth); void createScrollArea(); 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; static QScreen* getWidgetScreen(const QPoint& position); const CSMDoc::Document* getDocument() const; CSMDoc::Document* getDocument(); void setIndex(int viewIndex, int totalViews); void updateDocumentState(); void updateProgress(int current, int max, int type, int threads); void toggleStatusBar(bool checked); Operations* getOperations() const; signals: void newGameRequest(); void newAddonRequest(); void loadDocumentRequest(); void exitApplicationRequest(CSVDoc::View* view); void editSettingsRequest(); void mergeDocument(CSMDoc::Document* document); void requestFocus(const std::string& id); 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 abortOperation(int type); void updateTitle(); // called when subviews are added or removed void updateSubViewIndices(SubView* view = nullptr); private slots: void settingChanged(const CSMPrefs::Setting* setting); void undoActionChanged(); void redoActionChanged(); void newView(); void save(); void exit(); static void openHelp(); static void tutorial(); void infoAbout(); void infoAboutQt(); void verify(); void addGlobalsSubView(); void addGmstsSubView(); void addSkillsSubView(); void addClassesSubView(); void addFactionsSubView(); void addRacesSubView(); void addSoundsSubView(); void addScriptsSubView(); void addRegionsSubView(); void addBirthsignsSubView(); void addSpellsSubView(); void addCellsSubView(); void addReferenceablesSubView(); void addReferencesSubView(); void addRegionMapSubView(); void addFiltersSubView(); void addTopicsSubView(); void addJournalsSubView(); void addTopicInfosSubView(); void addJournalInfosSubView(); void addEnchantmentsSubView(); void addBodyPartsSubView(); void addSoundGensSubView(); void addMagicEffectsSubView(); void addMeshesSubView(); void addIconsSubView(); void addMusicsSubView(); void addSoundsResSubView(); void addTexturesSubView(); void addVideosSubView(); void addDebugProfilesSubView(); void addRunLogSubView(); void addLandsSubView(); void addLandTexturesSubView(); void addPathgridSubView(); void addStartScriptsSubView(); void addSearchSubView(); void addMetaDataSubView(); void toggleShowStatusBar(bool show); void loadErrorLog(); void run(const std::string& profile, const std::string& startupInstruction = ""); void stop(); void closeRequest(SubView* subView); void moveScrollBarToEnd(int min, int max); void merge(); void onRequestFocus(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/doc/viewmanager.cpp000066400000000000000000000403711503074453300234070ustar00rootroot00000000000000#include "viewmanager.hpp" #include #include #include #include #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/prefs/state.hpp" #include "../world/colordelegate.hpp" #include "../world/enumdelegate.hpp" #include "../world/idcompletiondelegate.hpp" #include "../world/idtypedelegate.hpp" #include "../world/recordstatusdelegate.hpp" #include "../world/vartypedelegate.hpp" #include "view.hpp" void CSVDoc::ViewManager::updateIndices() { std::map> documents; for (std::vector::const_iterator iter(mViews.begin()); iter != mViews.end(); ++iter) { 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; (*iter)->setIndex(document->second.first++, document->second.second); } } 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_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_RefRecordType, new CSVWorld::IdTypeDelegateFactory()); 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) { mDelegateFactories->add(*current, new CSVWorld::IdCompletionDelegateFactory()); } struct Mapping { CSMWorld::ColumnBase::Display mDisplay; CSMWorld::Columns::ColumnId mColumnId; bool mAllowNone; }; 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 }, { CSMWorld::ColumnBase::Display_ApparatusType, CSMWorld::Columns::ColumnId_ApparatusType, false }, { CSMWorld::ColumnBase::Display_ArmorType, CSMWorld::Columns::ColumnId_ArmorType, false }, { CSMWorld::ColumnBase::Display_ClothingType, CSMWorld::Columns::ColumnId_ClothingType, false }, { CSMWorld::ColumnBase::Display_CreatureType, CSMWorld::Columns::ColumnId_CreatureType, false }, { CSMWorld::ColumnBase::Display_WeaponType, CSMWorld::Columns::ColumnId_WeaponType, false }, { CSMWorld::ColumnBase::Display_DialogueType, CSMWorld::Columns::ColumnId_DialogueType, false }, { CSMWorld::ColumnBase::Display_QuestStatusType, CSMWorld::Columns::ColumnId_QuestStatusType, false }, { CSMWorld::ColumnBase::Display_EnchantmentType, CSMWorld::Columns::ColumnId_EnchantmentType, false }, { CSMWorld::ColumnBase::Display_BodyPartType, CSMWorld::Columns::ColumnId_BodyPartType, false }, { CSMWorld::ColumnBase::Display_MeshType, CSMWorld::Columns::ColumnId_MeshType, false }, { CSMWorld::ColumnBase::Display_Gender, CSMWorld::Columns::ColumnId_Gender, true }, { CSMWorld::ColumnBase::Display_SoundGeneratorType, CSMWorld::Columns::ColumnId_SoundGeneratorType, false }, { CSMWorld::ColumnBase::Display_School, CSMWorld::Columns::ColumnId_School, false }, { CSMWorld::ColumnBase::Display_SkillId, CSMWorld::Columns::ColumnId_Skill, true }, { CSMWorld::ColumnBase::Display_EffectRange, CSMWorld::Columns::ColumnId_EffectRange, false }, { CSMWorld::ColumnBase::Display_EffectId, CSMWorld::Columns::ColumnId_EffectId, false }, { CSMWorld::ColumnBase::Display_PartRefType, CSMWorld::Columns::ColumnId_PartRefType, false }, { CSMWorld::ColumnBase::Display_AiPackageType, CSMWorld::Columns::ColumnId_AiPackageType, false }, { CSMWorld::ColumnBase::Display_InfoCondFunc, CSMWorld::Columns::ColumnId_InfoCondFunc, false }, { CSMWorld::ColumnBase::Display_InfoCondComp, CSMWorld::Columns::ColumnId_InfoCondComp, false }, { CSMWorld::ColumnBase::Display_IngredEffectId, CSMWorld::Columns::ColumnId_EffectId, true }, { CSMWorld::ColumnBase::Display_EffectSkill, CSMWorld::Columns::ColumnId_Skill, false }, { CSMWorld::ColumnBase::Display_EffectAttribute, CSMWorld::Columns::ColumnId_Attribute, false }, { 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 }, }; 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, &CSMDoc::DocumentManager::loadRequest, &mLoader, &Loader::add); connect(&mDocumentManager, &CSMDoc::DocumentManager::loadingStopped, &mLoader, &Loader::loadingStopped); connect(&mDocumentManager, &CSMDoc::DocumentManager::nextStage, &mLoader, &Loader::nextStage); connect(&mDocumentManager, &CSMDoc::DocumentManager::nextRecord, &mLoader, &Loader::nextRecord); connect(&mDocumentManager, &CSMDoc::DocumentManager::loadMessage, &mLoader, &Loader::loadMessage); connect(&mLoader, &Loader::cancel, &mDocumentManager, &CSMDoc::DocumentManager::cancelLoading); connect(&mLoader, &Loader::close, &mDocumentManager, &CSMDoc::DocumentManager::removeDocument); } CSVDoc::ViewManager::~ViewManager() { delete mDelegateFactories; for (std::vector::iterator iter(mViews.begin()); iter != mViews.end(); ++iter) delete *iter; } CSVDoc::View* CSVDoc::ViewManager::addView(CSMDoc::Document* document) { if (countViews(document) == 0) { // new document connect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::documentStateChanged); connect(document, qOverload(&CSMDoc::Document::progress), this, &ViewManager::progress); } View* view = new View(*this, document, countViews(document) + 1); mViews.push_back(view); view->toggleStatusBar(CSMPrefs::get()["Windows"]["show-statusbar"].isTrue()); view->show(); 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) { View* view = addView(document); view->addSubView(id, hint); return view; } 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) ++count; return count; } bool CSVDoc::ViewManager::closeRequest(View* view) { std::vector::iterator iter = std::find(mViews.begin(), mViews.end(), view); bool continueWithClose = false; if (iter != mViews.end()) { bool last = countViews(view->getDocument()) <= 1; if (last) continueWithClose = notifySaveOnClose(view); else { (*iter)->deleteLater(); mViews.erase(iter); updateIndices(); } } return continueWithClose; } // NOTE: This method assumes that it is called only if the last document void CSVDoc::ViewManager::removeDocAndView(CSMDoc::Document* document) { for (std::vector::iterator iter(mViews.begin()); iter != mViews.end(); ++iter) { // the first match should also be the only match if ((*iter)->getDocument() == document) { mDocumentManager.removeDocument(document); (*iter)->deleteLater(); mViews.erase(iter); updateIndices(); return; } } } bool CSVDoc::ViewManager::notifySaveOnClose(CSVDoc::View* view) { bool result = true; CSMDoc::Document* document = view->getDocument(); // 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); return result; } bool CSVDoc::ViewManager::showModifiedDocumentMessageBox(CSVDoc::View* view) { emit closeMessageBox(); QMessageBox messageBox(view); 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, &ViewManager::closeMessageBox, &messageBox, &QMessageBox::close); connect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); mUserWarned = true; int response = messageBox.exec(); mUserWarned = false; switch (response) { case QMessageBox::Save: document->save(); mExitOnSaveStateChange = true; retVal = false; break; case QMessageBox::Discard: disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); break; case QMessageBox::Cancel: // disconnect to prevent unintended view closures disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); retVal = false; break; default: break; } return retVal; } bool CSVDoc::ViewManager::showSaveInProgressMessageBox(CSVDoc::View* view) { QMessageBox messageBox; CSMDoc::Document* document = view->getDocument(); 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); messageBox.setDefaultButton(waitButton); bool retVal = true; // 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. mUserWarned = true; messageBox.exec(); mUserWarned = false; // 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 mExitOnSaveStateChange = true; retVal = false; } else if (messageBox.clickedButton() == closeButton) { // disconnect to avoid segmentation fault disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); view->abortOperation(CSMDoc::State_Saving); mExitOnSaveStateChange = true; } else if (messageBox.clickedButton() == cancelButton) { // abort shutdown, allow save to complete // disconnection to prevent unintended view closures mExitOnSaveStateChange = false; disconnect(document, &CSMDoc::Document::stateChanged, this, &ViewManager::onExitWarningHandler); retVal = false; } return retVal; } 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(); } 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); } void CSVDoc::ViewManager::onExitWarningHandler(int state, CSMDoc::Document* document) { 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) emit closeMessageBox(); // 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) { 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) { if (document == (*iter)->getDocument()) (*iter)->setVisible(false); else remainingViews.push_back(*iter); } mDocumentManager.removeDocument(document); mViews = std::move(remainingViews); } return true; } void CSVDoc::ViewManager::exitApplication(CSVDoc::View* view) { if (!removeDocument(view)) // close the current document first return; 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())) return; } // Editor exits (via a signal) when the last document is deleted } openmw-openmw-0.49.0/apps/opencs/view/doc/viewmanager.hpp000066400000000000000000000042071503074453300234120ustar00rootroot00000000000000#ifndef CSV_DOC_VIEWMANAGER_H #define CSV_DOC_VIEWMANAGER_H #include #include #include #include "loader.hpp" namespace CSMDoc { class Document; class DocumentManager; } namespace CSVWorld { class CommandDelegateFactoryCollection; } namespace CSMWorld { class UniversalId; } namespace CSVDoc { class View; class ViewManager : public QObject { Q_OBJECT CSMDoc::DocumentManager& mDocumentManager; std::vector mViews; CSVWorld::CommandDelegateFactoryCollection* mDelegateFactories; bool mExitOnSaveStateChange; bool mUserWarned; Loader mLoader; // 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); public: ViewManager(CSMDoc::DocumentManager& documentManager); ~ViewManager() override; 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); int countViews(const CSMDoc::Document* document) const; ///< Return number of views for \a document. bool closeRequest(View* view); void removeDocAndView(CSMDoc::Document* document); signals: void newGameRequest(); void newAddonRequest(); void loadDocumentRequest(); void closeMessageBox(); void editSettingsRequest(); void mergeDocument(CSMDoc::Document* document); public slots: void exitApplication(CSVDoc::View* view); private slots: void documentStateChanged(int state, CSMDoc::Document* document); void progress(int current, int max, int type, int threads, CSMDoc::Document* document); void onExitWarningHandler(int state, CSMDoc::Document* document); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/filter/000077500000000000000000000000001503074453300211115ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/filter/editwidget.cpp000066400000000000000000000171111503074453300237470ustar00rootroot00000000000000#include "editwidget.hpp" #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" CSVFilter::EditWidget::EditWidget(CSMWorld::Data& data, QWidget* parent) : QLineEdit(parent) , mParser(data) , mIsEmpty(true) { mPalette = palette(); 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) { // 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 mIsEmpty = false; if (mParser.parse(text.toUtf8().constData())) { setPalette(mPalette); emit filterChanged(mParser.getFilter()); } else { 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) { for (int i = topLeft.column(); i <= bottomRight.column(); ++i) if (i != mStateColumnIndex && i != mDescColumnIndex) textChanged(text()); } void CSVFilter::EditWidget::filterRowsRemoved(const QModelIndex& parent, int start, int end) { textChanged(text()); } void CSVFilter::EditWidget::filterRowsInserted(const QModelIndex& parent, int start, int end) { textChanged(text()); } void CSVFilter::EditWidget::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { 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; { case 0: // empty return; // nothing to do here case 1: // only single multipleElements = false; break; default: multipleElements = true; break; } Qt::KeyboardModifiers key = QApplication::keyboardModifiers(); QString oldContent(text()); bool replaceMode = false; std::string orAnd; switch (key) // setting replaceMode and string used to glue expressions { case Qt::ShiftModifier: orAnd = "!or("; replaceMode = false; break; case Qt::ControlModifier: orAnd = "!and("; replaceMode = false; break; default: replaceMode = true; break; } 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('!'); } std::stringstream ss; if (multipleElements) { if (replaceMode) { ss << "!or("; } else { ss << orAnd << oldContent.toUtf8().constData() << ','; } for (unsigned i = 0; i < count; ++i) { ss << generateFilter(newFilter[i], filterType); if (i + 1 != count) { ss << ", "; } } ss << ')'; } else { if (!replaceMode) { ss << orAnd << oldContent.toUtf8().constData() << ','; } else { ss << '!'; } ss << generateFilter(newFilter[0], filterType); if (!replaceMode) { ss << ')'; } } if (ss.str().length() > 4) { clear(); insert(QString::fromUtf8(ss.str().c_str())); } } std::string CSVFilter::EditWidget::generateFilter(const FilterData& filterData, FilterType filterType) const { const unsigned columns = filterData.columns.size(); bool multipleColumns = false; switch (columns) { case 0: // empty return ""; // no column to filter case 1: // one column to look for multipleColumns = false; 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("; for (unsigned i = 0; i < columns; ++i) { ss << filterTypeName(filterType) << "(" << '"' << filterData.columns[i] << '"' << ',' << quotesResolved << ')'; if (i + 1 != columns) ss << ','; } ss << ')'; } else { ss << filterTypeName(filterType) << '(' << '"' << filterData.columns[0] << "\"," << quotesResolved << ")"; } return ss.str(); } void CSVFilter::EditWidget::contextMenuEvent(QContextMenuEvent* event) { QMenu* menu = createStandardContextMenu(); menu->addAction(mHelpAction); menu->exec(event->globalPos()); delete menu; } void CSVFilter::EditWidget::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/record-filters.html"); } openmw-openmw-0.49.0/apps/opencs/view/filter/editwidget.hpp000066400000000000000000000054141503074453300237570ustar00rootroot00000000000000#ifndef CSV_FILTER_EDITWIDGET_H #define CSV_FILTER_EDITWIDGET_H #include #include #include #include #include #include #include #include #include "filterdata.hpp" #include "../../model/filter/parser.hpp" class QModelIndex; class QAction; class QContextMenuEvent; class QObject; class QWidget; namespace CSMFilter { class Node; } namespace CSMWorld { class Data; } namespace CSVFilter { enum class FilterType { 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); } }; class EditWidget : public QLineEdit { Q_OBJECT CSMFilter::Parser mParser; QPalette mPalette; bool mIsEmpty; int mStateColumnIndex; int mDescColumnIndex; QAction* mHelpAction; public: EditWidget(CSMWorld::Data& data, QWidget* parent = nullptr); void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); signals: void filterChanged(std::shared_ptr filter); private: std::string generateFilter(const FilterData& filterData, FilterType filterType) const; void contextMenuEvent(QContextMenuEvent* event) override; constexpr std::string_view filterTypeName(const FilterType& type) const { switch (type) { case FilterType::String: return "string"; case FilterType::Value: return "value"; } return "unknown type"; } private slots: void textChanged(const QString& text); void filterDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void filterRowsRemoved(const QModelIndex& parent, int start, int end); void filterRowsInserted(const QModelIndex& parent, int start, int end); static void openHelp(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/filter/filterbox.cpp000066400000000000000000000041771503074453300236240ustar00rootroot00000000000000#include "filterbox.hpp" #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) { QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); mRecordFilterBox = new RecordFilterBox(data, this); layout->addWidget(mRecordFilterBox); setLayout(layout); connect(mRecordFilterBox, &RecordFilterBox::filterChanged, this, &FilterBox::recordFilterChanged); setAcceptDrops(true); } void CSVFilter::FilterBox::setRecordFilter(const std::string& filter) { mRecordFilterBox->setFilter(filter); } void CSVFilter::FilterBox::dropEvent(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; std::vector universalIdData = mime->getData(); 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) { event->acceptProposedAction(); } void CSVFilter::FilterBox::dragMoveEvent(QDragMoveEvent* event) { event->accept(); } void CSVFilter::FilterBox::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { mRecordFilterBox->createFilterRequest(sourceFilter, action); } openmw-openmw-0.49.0/apps/opencs/view/filter/filterbox.hpp000066400000000000000000000023561503074453300236260ustar00rootroot00000000000000#ifndef CSV_FILTER_FILTERBOX_H #define CSV_FILTER_FILTERBOX_H #include #include #include #include #include #include #include #include "filterdata.hpp" class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QObject; namespace CSMFilter { class Node; } namespace CSMWorld { class Data; class UniversalId; } namespace CSVFilter { class RecordFilterBox; class FilterBox : public QWidget { Q_OBJECT RecordFilterBox* mRecordFilterBox; public: FilterBox(CSMWorld::Data& data, QWidget* parent = nullptr); void setRecordFilter(const std::string& filter); void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; signals: void recordFilterChanged(std::shared_ptr filter); void recordDropped(std::vector& types, const std::pair& columnSearchData, Qt::DropAction action); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/filter/filterdata.hpp000066400000000000000000000004631503074453300237440ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/view/filter/recordfilterbox.cpp000066400000000000000000000017431503074453300250170ustar00rootroot00000000000000#include "recordfilterbox.hpp" #include #include #include #include #include "editwidget.hpp" #include "filterdata.hpp" CSVFilter::RecordFilterBox::RecordFilterBox(CSMWorld::Data& data, QWidget* parent) : QWidget(parent) { QHBoxLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 6, 5, 0); QLabel* label = new QLabel("Record Filter", this); label->setIndent(2); layout->addWidget(label); mEdit = new EditWidget(data, this); layout->addWidget(mEdit); setLayout(layout); connect(mEdit, &EditWidget::filterChanged, this, &RecordFilterBox::filterChanged); } void CSVFilter::RecordFilterBox::setFilter(const std::string& filter) { mEdit->clear(); mEdit->setText(QString::fromUtf8(filter.c_str())); } void CSVFilter::RecordFilterBox::createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action) { mEdit->createFilterRequest(sourceFilter, action); } openmw-openmw-0.49.0/apps/opencs/view/filter/recordfilterbox.hpp000066400000000000000000000015361503074453300250240ustar00rootroot00000000000000#ifndef CSV_FILTER_RECORDFILTERBOX_H #define CSV_FILTER_RECORDFILTERBOX_H #include #include #include #include #include #include #include #include "filterdata.hpp" namespace CSMFilter { class Node; } namespace CSMWorld { class Data; } namespace CSVFilter { class EditWidget; class RecordFilterBox : public QWidget { Q_OBJECT EditWidget* mEdit; public: RecordFilterBox(CSMWorld::Data& data, QWidget* parent = nullptr); void setFilter(const std::string& filter); void useFilterRequest(const std::string& idOfFilter); void createFilterRequest(const std::vector& sourceFilter, Qt::DropAction action); signals: void filterChanged(std::shared_ptr filter); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/prefs/000077500000000000000000000000001503074453300207435ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/prefs/contextmenulist.cpp000066400000000000000000000020771503074453300247220ustar00rootroot00000000000000#include "contextmenulist.hpp" #include #include #include #include "../../model/prefs/state.hpp" CSVPrefs::ContextMenuList::ContextMenuList(QWidget* parent) : QListWidget(parent) { } void CSVPrefs::ContextMenuList::contextMenuEvent(QContextMenuEvent* e) { QMenu* menu = new QMenu(); menu->addAction("Reset category to default", this, SLOT(resetCategory())); menu->addAction("Reset all to default", this, SLOT(resetAll())); menu->exec(e->globalPos()); delete menu; } void CSVPrefs::ContextMenuList::mousePressEvent(QMouseEvent* e) { // enable all buttons except right click // This means that when right-clicking to enable the // context menu, the page doesn't switch at the same time. if (!(e->buttons() & Qt::RightButton)) { QListWidget::mousePressEvent(e); } } void CSVPrefs::ContextMenuList::resetCategory() { CSMPrefs::State::get().resetCategory(currentItem()->text().toStdString()); } void CSVPrefs::ContextMenuList::resetAll() { CSMPrefs::State::get().resetAll(); } openmw-openmw-0.49.0/apps/opencs/view/prefs/contextmenulist.hpp000066400000000000000000000010161503074453300247170ustar00rootroot00000000000000#ifndef CSV_PREFS_CONTEXTMENULIST_H #define CSV_PREFS_CONTEXTMENULIST_H #include class QContextMenuEvent; class QMouseEvent; 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/prefs/dialogue.cpp000066400000000000000000000073141503074453300232450ustar00rootroot00000000000000#include "dialogue.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../../model/prefs/state.hpp" #include "contextmenulist.hpp" #include "keybindingpage.hpp" #include "page.hpp" void CSVPrefs::Dialogue::buildCategorySelector(QSplitter* main) { CSVPrefs::ContextMenuList* list = new CSVPrefs::ContextMenuList(main); list->setMinimumWidth(50); list->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); list->setSelectionBehavior(QAbstractItemView::SelectItems); main->addWidget(list); QFontMetrics metrics(QApplication::font(list)); int maxWidth = 1; for (CSMPrefs::State::Iterator iter = CSMPrefs::get().begin(); iter != CSMPrefs::get().end(); ++iter) { QString label = QString::fromUtf8(iter->second.getKey().c_str()); maxWidth = std::max(maxWidth, metrics.horizontalAdvance(label)); list->addItem(label); } list->setMaximumWidth(maxWidth + 50); connect(list, &ContextMenuList::currentItemChanged, this, &Dialogue::selectionChanged); } void CSVPrefs::Dialogue::buildContentArea(QSplitter* main) { mContent = new QStackedWidget(main); mContent->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); main->addWidget(mContent); } 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); } CSVPrefs::Dialogue::Dialogue() { setWindowTitle("User Settings"); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setMinimumSize(600, 400); resize(810, 680); QSplitter* main = new QSplitter(this); setCentralWidget(main); buildCategorySelector(main); buildContentArea(main); } CSVPrefs::Dialogue::~Dialogue() { try { if (isVisible()) CSMPrefs::State::get().save(); } catch (const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void CSVPrefs::Dialogue::closeEvent(QCloseEvent* event) { QMainWindow::closeEvent(event); CSMPrefs::State::get().save(); } void CSVPrefs::Dialogue::show() { 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); } else { QRect scr = QGuiApplication::primaryScreen()->geometry(); // otherwise place at the centre of the screen QPoint screenCenter = scr.center(); move(screenCenter - QPoint(frameGeometry().width() / 2, frameGeometry().height() / 2)); } QWidget::show(); } void CSVPrefs::Dialogue::selectionChanged(QListWidgetItem* current, QListWidgetItem* previous) { if (current) { std::string key = current->text().toUtf8().data(); for (int i = 0; i < mContent->count(); ++i) { PageBase& page = dynamic_cast(*mContent->widget(i)); if (page.getCategory().getKey() == key) { mContent->setCurrentIndex(i); return; } } PageBase* page = makePage(key); mContent->setCurrentIndex(mContent->addWidget(page)); } } openmw-openmw-0.49.0/apps/opencs/view/prefs/dialogue.hpp000066400000000000000000000013601503074453300232450ustar00rootroot00000000000000#ifndef CSV_PREFS_DIALOGUE_H #define CSV_PREFS_DIALOGUE_H #include class QSplitter; class QStackedWidget; class QListWidgetItem; namespace CSVPrefs { class PageBase; class Dialogue : public QMainWindow { Q_OBJECT QStackedWidget* mContent; private: void buildCategorySelector(QSplitter* main); void buildContentArea(QSplitter* main); PageBase* makePage(const std::string& key); public: Dialogue(); ~Dialogue() override; protected: void closeEvent(QCloseEvent* event) override; public slots: void show(); private slots: void selectionChanged(QListWidgetItem* current, QListWidgetItem* previous); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/prefs/keybindingpage.cpp000066400000000000000000000061741503074453300244370ustar00rootroot00000000000000#include "keybindingpage.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../../model/prefs/category.hpp" #include "../../model/prefs/setting.hpp" #include "../../model/prefs/state.hpp" namespace CSVPrefs { KeyBindingPage::KeyBindingPage(CSMPrefs::Category& category, QWidget* parent) : PageBase(category, parent) , mStackedLayout(nullptr) , mPageLayout(nullptr) , mPageSelector(nullptr) { // Need one widget for scroll area QWidget* topWidget = new QWidget(); QVBoxLayout* topLayout = new QVBoxLayout(topWidget); // Allows switching between "pages" QWidget* stackedWidget = new QWidget(); mStackedLayout = new QStackedLayout(stackedWidget); mPageSelector = new QComboBox(); 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, &QPushButton::clicked, this, &KeyBindingPage::resetKeyBindings); topLayout->addWidget(mPageSelector); topLayout->addWidget(stackedWidget); topLayout->addWidget(lineSeparator); topLayout->addWidget(resetButton); topLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); // Add each option for (CSMPrefs::Category::Iterator iter = category.begin(); iter != category.end(); ++iter) addSetting(*iter); setWidgetResizable(true); setWidget(topWidget); } void KeyBindingPage::addSetting(CSMPrefs::Setting* setting) { const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { // Label, Option widgets assert(mPageLayout); int next = mPageLayout->rowCount(); mPageLayout->addWidget(widgets.mLabel, next, 0); mPageLayout->addWidget(widgets.mInput, next, 1); } else if (widgets.mInput != nullptr) { // Wide single widget assert(mPageLayout); int next = mPageLayout->rowCount(); mPageLayout->addWidget(widgets.mInput, next, 0, 1, 2); } else { // Create new page QWidget* pageWidget = new QWidget(); mPageLayout = new QGridLayout(pageWidget); mPageLayout->setSizeConstraint(QLayout::SetMinAndMaxSize); mStackedLayout->addWidget(pageWidget); mPageSelector->addItem(setting->getLabel()); } } void KeyBindingPage::resetKeyBindings() { CSMPrefs::State::get().resetCategory("Key Bindings"); } } openmw-openmw-0.49.0/apps/opencs/view/prefs/keybindingpage.hpp000066400000000000000000000012131503074453300244310ustar00rootroot00000000000000#ifndef CSV_PREFS_KEYBINDINGPAGE_H #define CSV_PREFS_KEYBINDINGPAGE_H #include "pagebase.hpp" class QComboBox; class QGridLayout; class QStackedLayout; class QWidget; namespace CSMPrefs { class Setting; class Category; } namespace CSVPrefs { class KeyBindingPage : public PageBase { Q_OBJECT public: KeyBindingPage(CSMPrefs::Category& category, QWidget* parent); void addSetting(CSMPrefs::Setting* setting); private: QStackedLayout* mStackedLayout; QGridLayout* mPageLayout; QComboBox* mPageSelector; private slots: void resetKeyBindings(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/prefs/page.cpp000066400000000000000000000020021503074453300223550ustar00rootroot00000000000000#include "page.hpp" #include #include #include #include #include #include "../../model/prefs/category.hpp" #include "../../model/prefs/setting.hpp" CSVPrefs::Page::Page(CSMPrefs::Category& category, QWidget* parent) : PageBase(category, parent) { QWidget* widget = new QWidget(parent); mGrid = new QGridLayout(widget); for (CSMPrefs::Category::Iterator iter = category.begin(); iter != category.end(); ++iter) addSetting(*iter); setWidget(widget); } void CSVPrefs::Page::addSetting(CSMPrefs::Setting* setting) { const CSMPrefs::SettingWidgets widgets = setting->makeWidgets(this); int next = mGrid->rowCount(); if (widgets.mLabel != nullptr && widgets.mInput != nullptr) { mGrid->addWidget(widgets.mLabel, next, 0); mGrid->addWidget(widgets.mInput, next, 1); } else if (widgets.mInput != nullptr) { mGrid->addWidget(widgets.mInput, next, 0, 1, 2); } } openmw-openmw-0.49.0/apps/opencs/view/prefs/page.hpp000066400000000000000000000006701503074453300223730ustar00rootroot00000000000000#ifndef CSV_PREFS_PAGE_H #define CSV_PREFS_PAGE_H #include "pagebase.hpp" class QGridLayout; class QWidget; class QObject; namespace CSMPrefs { class Category; class Setting; } namespace CSVPrefs { class Page : public PageBase { Q_OBJECT QGridLayout* mGrid; public: Page(CSMPrefs::Category& category, QWidget* parent); void addSetting(CSMPrefs::Setting* setting); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/prefs/pagebase.cpp000066400000000000000000000015331503074453300232200ustar00rootroot00000000000000 #include "pagebase.hpp" #include #include #include "../../model/prefs/category.hpp" #include "../../model/prefs/state.hpp" CSVPrefs::PageBase::PageBase(CSMPrefs::Category& category, QWidget* parent) : QScrollArea(parent) , mCategory(category) { } CSMPrefs::Category& CSVPrefs::PageBase::getCategory() { return mCategory; } void CSVPrefs::PageBase::contextMenuEvent(QContextMenuEvent* e) { QMenu* menu = new QMenu(); menu->addAction("Reset category to default", this, SLOT(resetCategory())); menu->addAction("Reset all to default", this, SLOT(resetAll())); menu->exec(e->globalPos()); delete menu; } void CSVPrefs::PageBase::resetCategory() { CSMPrefs::State::get().resetCategory(getCategory().getKey()); } void CSVPrefs::PageBase::resetAll() { CSMPrefs::State::get().resetAll(); } openmw-openmw-0.49.0/apps/opencs/view/prefs/pagebase.hpp000066400000000000000000000010711503074453300232220ustar00rootroot00000000000000#ifndef CSV_PREFS_PAGEBASE_H #define CSV_PREFS_PAGEBASE_H #include class QContextMenuEvent; namespace CSMPrefs { class Category; } namespace CSVPrefs { class PageBase : public QScrollArea { Q_OBJECT CSMPrefs::Category& mCategory; public: PageBase(CSMPrefs::Category& category, QWidget* parent); CSMPrefs::Category& getCategory(); protected: void contextMenuEvent(QContextMenuEvent*) override; private slots: void resetCategory(); void resetAll(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/000077500000000000000000000000001503074453300211035ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/render/actor.cpp000066400000000000000000000103561503074453300227240ustar00rootroot00000000000000#include "actor.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/world/data.hpp" namespace CSVRender { const std::string Actor::MeshPrefix = "meshes\\"; Actor::Actor(const ESM::RefId& id, CSMWorld::Data& data) : mId(id) , mData(data) , mBaseNode(new osg::PositionAttitudeTransform()) , mSkeleton(nullptr) { mActorData = mData.getActorAdapter()->getActorData(mId); connect(mData.getActorAdapter(), &CSMWorld::ActorAdapter::actorChanged, this, &Actor::handleActorChanged); } osg::Group* Actor::getBaseNode() { return mBaseNode; } void Actor::update() { mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); // Load skeleton VFS::Path::Normalized skeletonModel = mActorData->getSkeleton(); skeletonModel = Misc::ResourceHelpers::correctActorModelPath(skeletonModel, mData.getResourceSystem()->getVFS()); loadSkeleton(skeletonModel); if (!mActorData->isCreature()) { // Get rid of the extra attachments SceneUtil::CleanObjectRootVisitor cleanVisitor; mSkeleton->accept(cleanVisitor); cleanVisitor.remove(); // Attach parts to skeleton loadBodyParts(); const osg::Vec2f& attributes = mActorData->getRaceWeightHeight(); mBaseNode->setScale(osg::Vec3d(attributes.x(), attributes.x(), attributes.y())); } else { SceneUtil::RemoveTriBipVisitor removeTriBipVisitor; mSkeleton->accept(removeTriBipVisitor); removeTriBipVisitor.remove(); } // Post setup mSkeleton->markDirty(); mSkeleton->setActive(SceneUtil::Skeleton::Active); } void Actor::handleActorChanged(const ESM::RefId& refId) { if (mId == refId) { update(); } } void Actor::loadSkeleton(const std::string& model) { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); osg::ref_ptr temp = sceneMgr->getInstance(VFS::Path::toNormalized(model)); mSkeleton = dynamic_cast(temp.get()); if (!mSkeleton) { mSkeleton = new SceneUtil::Skeleton(); mSkeleton->addChild(temp); } mBaseNode->addChild(mSkeleton); // Map bone names to bones mNodeMap.clear(); SceneUtil::NodeMapVisitor nmVisitor(mNodeMap); mSkeleton->accept(nmVisitor); } void Actor::loadBodyParts() { for (int i = 0; i < ESM::PRT_Count; ++i) { const auto type = static_cast(i); attachBodyPart(type, getBodyPartMesh(mActorData->getPart(type))); } } void Actor::attachBodyPart(ESM::PartReferenceType type, const std::string& mesh) { auto sceneMgr = mData.getResourceSystem()->getSceneManager(); // Attach to skeleton std::string boneName = ESM::getBoneName(type); auto node = mNodeMap.find(boneName); if (!mesh.empty() && node != mNodeMap.end()) { auto instance = sceneMgr->getInstance(VFS::Path::toNormalized(mesh)); SceneUtil::attach(instance, mSkeleton, boneName, node->second, sceneMgr); } } std::string Actor::getBodyPartMesh(const ESM::RefId& bodyPartId) { const auto& bodyParts = mData.getBodyParts(); const int index = bodyParts.searchId(bodyPartId); if (index != -1 && !bodyParts.getRecord(index).isDeleted()) return MeshPrefix + bodyParts.getRecord(index).get().mModel; else return ""; } } openmw-openmw-0.49.0/apps/opencs/view/render/actor.hpp000066400000000000000000000031141503074453300227230ustar00rootroot00000000000000#ifndef OPENCS_VIEW_RENDER_ACTOR_H #define OPENCS_VIEW_RENDER_ACTOR_H #include #include #include #include #include #include #include #include #include "../../model/world/actoradapter.hpp" namespace CSMWorld { class Data; } namespace SceneUtil { class Skeleton; } namespace CSVRender { /// Handles loading an npc or creature class Actor : public QObject { Q_OBJECT public: /// Creates an actor. /// \param id The referenceable id /// \param type The record type /// \param data The data store Actor(const ESM::RefId& id, CSMWorld::Data& data); /// Retrieves the base node that meshes are attached to osg::Group* getBaseNode(); /// (Re)creates the npc or creature renderable void update(); private slots: 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 ESM::RefId& bodyPartId); static const std::string MeshPrefix; ESM::RefId mId; CSMWorld::Data& mData; CSMWorld::ActorAdapter::ActorDataPtr mActorData; osg::ref_ptr mBaseNode; SceneUtil::Skeleton* mSkeleton; SceneUtil::NodeMap mNodeMap; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/brushdraw.cpp000066400000000000000000000262501503074453300236150ustar00rootroot00000000000000#include "brushdraw.hpp" #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(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); else mLandSizeFactor = static_cast(ESM::Land::REAL_SIZE) / static_cast(ESM::Land::LAND_SIZE - 1); } CSVRender::BrushDraw::~BrushDraw() { mBrushDrawNode->removeChild(mGeometry); mParentNode->removeChild(mBrushDrawNode); } float CSVRender::BrushDraw::getIntersectionHeight(const osg::Vec3d& point) { osg::Vec3d start = point; osg::Vec3d end = point; start.z() = std::numeric_limits::max(); end.z() = std::numeric_limits::lowest(); osg::Vec3d direction = end - start; // Get intersection osg::ref_ptr intersector( new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(Mask_Terrain); mParentNode->accept(visitor); for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); it != intersector->getIntersections().end(); ++it) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; // reject back-facing polygons if (direction * intersection.getWorldIntersectNormal() > 0) { continue; } return intersection.getWorldIntersectPoint().z(); } return 0.0f; } 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::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)); 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)); 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)); 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)); colors->push_back(lineColor); geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, 4)); mGeometry = geom; } 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()); 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; // 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++) { 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); vertices->push_back(upHorizontalLinePoint1); colors->push_back(lineColor); vertices->push_back(upHorizontalLinePoint2); colors->push_back(lineColor); vertices->push_back(upVerticalLinePoint1); colors->push_back(lineColor); vertices->push_back(upVerticalLinePoint2); colors->push_back(lineColor); vertices->push_back(downHorizontalLinePoint1); colors->push_back(lineColor); vertices->push_back(downHorizontalLinePoint2); colors->push_back(lineColor); vertices->push_back(downVerticalLinePoint1); colors->push_back(lineColor); vertices->push_back(downVerticalLinePoint2); colors->push_back(lineColor); } geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINES, 0, resolution * 8)); mGeometry = geom; } 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 = 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)); 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)); colors->push_back(lineColor); } geom->setVertexArray(vertices); geom->setColorArray(colors, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::LINE_STRIP, 0, amountOfPoints * 2)); mGeometry = geom; } void CSVRender::BrushDraw::buildCustomGeometry(const float& radius, const osg::Vec3d& point) { // Not implemented } void CSVRender::BrushDraw::update(osg::Vec3d point, int brushSize, CSVWidget::BrushShape toolShape) { if (mBrushDrawNode->containsNode(mGeometry)) mBrushDrawNode->removeChild(mGeometry); float radius = (mLandSizeFactor * brushSize) / 2; osg::Vec3d snapToGridPoint = point; if (mTextureMode) { std::pair snapToGridXY = CSMWorld::CellCoordinates::toTextureCoords(point); float offsetToMiddle = mLandSizeFactor * 0.5f; snapToGridPoint = osg::Vec3d( CSMWorld::CellCoordinates::textureGlobalXToWorldCoords(snapToGridXY.first) + offsetToMiddle, 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()); } switch (toolShape) { case (CSVWidget::BrushShape_Point): buildPointGeometry(snapToGridPoint); break; case (CSVWidget::BrushShape_Square): buildSquareGeometry(radius, snapToGridPoint); break; case (CSVWidget::BrushShape_Circle): buildCircleGeometry(radius, snapToGridPoint); break; case (CSVWidget::BrushShape_Custom): buildSquareGeometry(1, snapToGridPoint); // buildCustomGeometry break; } mGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mBrushDrawNode->addChild(mGeometry); } void CSVRender::BrushDraw::hide() { if (mBrushDrawNode->containsNode(mGeometry)) mBrushDrawNode->removeChild(mGeometry); } openmw-openmw-0.49.0/apps/opencs/view/render/brushdraw.hpp000066400000000000000000000020531503074453300236150ustar00rootroot00000000000000#ifndef CSV_RENDER_BRUSHDRAW_H #define CSV_RENDER_BRUSHDRAW_H #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; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cameracontroller.cpp000066400000000000000000000545721503074453300251600ustar00rootroot00000000000000#include "cameracontroller.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/prefs/shortcut.hpp" namespace CSVRender { /* Camera Controller */ const osg::Vec3d CameraController::WorldUp = osg::Vec3d(0, 0, 1); const osg::Vec3d CameraController::LocalUp = osg::Vec3d(0, 1, 0); const osg::Vec3d CameraController::LocalLeft = osg::Vec3d(1, 0, 0); const osg::Vec3d CameraController::LocalForward = osg::Vec3d(0, 0, 1); CameraController::CameraController(QObject* parent) : QObject(parent) , mActive(false) , mInverted(false) , mCameraSensitivity(1 / 650.f) , mSecondaryMoveMult(50) , mWheelMoveMult(8) , mCamera(nullptr) { } bool CameraController::isActive() const { return mActive; } osg::Camera* CameraController::getCamera() const { return mCamera; } double CameraController::getCameraSensitivity() const { return mCameraSensitivity; } bool CameraController::getInverted() const { return mInverted; } double CameraController::getSecondaryMovementMultiplier() const { return mSecondaryMoveMult; } double CameraController::getWheelMovementMultiplier() const { return mWheelMoveMult; } void CameraController::setCamera(osg::Camera* camera) { bool wasActive = mActive; mCamera = camera; mActive = (mCamera != nullptr); if (mActive != wasActive) { for (std::vector::iterator it = mShortcuts.begin(); it != mShortcuts.end(); ++it) { CSMPrefs::Shortcut* shortcut = *it; shortcut->enable(mActive); } } } void CameraController::setCameraSensitivity(double value) { mCameraSensitivity = value; } void CameraController::setInverted(bool value) { mInverted = value; } void CameraController::setSecondaryMovementMultiplier(double value) { mSecondaryMoveMult = value; } void CameraController::setWheelMovementMultiplier(double value) { mWheelMoveMult = value; } void CameraController::setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up) { // Find World bounds osg::ComputeBoundsVisitor boundsVisitor; osg::BoundingBox& boundingBox = boundsVisitor.getBoundingBox(); boundsVisitor.setTraversalMask(mask); root->accept(boundsVisitor); if (!boundingBox.valid()) { // Try again without any mask boundsVisitor.reset(); boundsVisitor.setTraversalMask(~0u); root->accept(boundsVisitor); // Last resort, set a default if (!boundingBox.valid()) { boundingBox.set(-1, -1, -1, 1, 1, 1); } } // Calculate a good starting position osg::Vec3d minBounds = boundingBox.corner(0) - boundingBox.center(); osg::Vec3d maxBounds = boundingBox.corner(7) - boundingBox.center(); osg::Vec3d camOffset = up * maxBounds > 0 ? maxBounds : minBounds; camOffset *= 2; osg::Vec3d eye = camOffset + boundingBox.center(); osg::Vec3d center = boundingBox.center(); getCamera()->setViewMatrixAsLookAt(eye, center, up); } void CameraController::addShortcut(CSMPrefs::Shortcut* shortcut) { mShortcuts.push_back(shortcut); } /* Free Camera Controller */ FreeCameraController::FreeCameraController(QWidget* widget) : CameraController(widget) , mLockUpright(false) , mModified(false) , mNaviPrimary(false) , mNaviSecondary(false) , mFast(false) , mFastAlternate(false) , mLeft(false) , mRight(false) , mForward(false) , mBackward(false) , mRollLeft(false) , mRollRight(false) , mUp(LocalUp) , mLinSpeed(1000) , mRotSpeed(osg::PI / 2) , mSpeedMult(8) { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); 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, 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); forwardShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::left); addShortcut(leftShortcut); CSMPrefs::Shortcut* backShortcut = new CSMPrefs::Shortcut("free-backward", widget); backShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::right); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("free-roll-left", widget); rollLeftShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::rollRight); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("free-speed-mode", widget); speedModeShortcut->enable(false); connect( speedModeShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &FreeCameraController::swapSpeedMode); addShortcut(speedModeShortcut); } double FreeCameraController::getLinearSpeed() const { return mLinSpeed; } double FreeCameraController::getRotationalSpeed() const { return mRotSpeed; } double FreeCameraController::getSpeedMultiplier() const { return mSpeedMult; } void FreeCameraController::setLinearSpeed(double value) { mLinSpeed = value; } void FreeCameraController::setRotationalSpeed(double value) { mRotSpeed = value; } void FreeCameraController::setSpeedMultiplier(double value) { mSpeedMult = value; } void FreeCameraController::fixUpAxis(const osg::Vec3d& up) { mLockUpright = true; mUp = up; mModified = true; } void FreeCameraController::unfixUpAxis() { mLockUpright = false; } void FreeCameraController::handleMouseMoveEvent(int x, int y) { if (!isActive()) return; if (mNaviPrimary) { double scalar = getCameraSensitivity() * (getInverted() ? -1.0 : 1.0); yaw(x * scalar); pitch(y * scalar); } else if (mNaviSecondary) { osg::Vec3d movement; movement += LocalLeft * -x * getSecondaryMovementMultiplier(); movement += LocalUp * y * getSecondaryMovementMultiplier(); translate(movement); } } void FreeCameraController::handleMouseScrollEvent(int x) { if (!isActive()) return; translate(LocalForward * x * ((mFast ^ mFastAlternate) ? getWheelMovementMultiplier() : 1)); } void FreeCameraController::update(double dt) { if (!isActive()) return; double linDist = mLinSpeed * dt; double rotDist = mRotSpeed * dt; if (mFast ^ mFastAlternate) linDist *= mSpeedMult; if (mLeft) translate(LocalLeft * linDist); if (mRight) translate(LocalLeft * -linDist); if (mForward) translate(LocalForward * linDist); if (mBackward) translate(LocalForward * -linDist); if (!mLockUpright) { if (mRollLeft) roll(-rotDist); if (mRollRight) roll(rotDist); } else if (mModified) { stabilize(); mModified = false; } // Normalize the matrix to counter drift getCamera()->getViewMatrix().orthoNormal(getCamera()->getViewMatrix()); } void FreeCameraController::yaw(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalUp); mModified = true; } void FreeCameraController::pitch(double value) { const double Constraint = osg::PI / 2 - 0.1; if (mLockUpright) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d forward = center - eye; osg::Vec3d left = up ^ forward; double pitchAngle = std::acos(up * mUp); if ((mUp ^ up) * left < 0) pitchAngle *= -1; if (std::abs(pitchAngle + value) > Constraint) value = (pitchAngle > 0 ? 1 : -1) * Constraint - pitchAngle; } getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalLeft); mModified = true; } void FreeCameraController::roll(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalForward); mModified = true; } void FreeCameraController::translate(const osg::Vec3d& offset) { getCamera()->getViewMatrix() *= osg::Matrixd::translate(offset); mModified = true; } void FreeCameraController::stabilize() { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); getCamera()->setViewMatrixAsLookAt(eye, center, mUp); } void FreeCameraController::naviPrimary(bool active) { mNaviPrimary = active; } void FreeCameraController::naviSecondary(bool active) { mNaviSecondary = active; } void FreeCameraController::forward(bool active) { mForward = active; } void FreeCameraController::left(bool active) { mLeft = active; } void FreeCameraController::backward(bool active) { mBackward = active; } void FreeCameraController::right(bool active) { mRight = active; } void FreeCameraController::rollLeft(bool active) { mRollLeft = active; } void FreeCameraController::rollRight(bool active) { mRollRight = active; } void FreeCameraController::alternateFast(bool active) { mFastAlternate = active; } void FreeCameraController::swapSpeedMode() { mFast = !mFast; } /* Orbit Camera Controller */ OrbitCameraController::OrbitCameraController(QWidget* widget) : CameraController(widget) , mInitialized(false) , mNaviPrimary(false) , mNaviSecondary(false) , mFast(false) , mFastAlternate(false) , mLeft(false) , mRight(false) , mUp(false) , mDown(false) , mRollLeft(false) , mRollRight(false) , mPickingMask(~0u) , mCenter(0, 0, 0) , mDistance(0) , mOrbitSpeed(osg::PI / 4) , mOrbitSpeedMult(4) , mConstRoll(false) { CSMPrefs::Shortcut* naviPrimaryShortcut = new CSMPrefs::Shortcut("scene-navi-primary", widget); naviPrimaryShortcut->enable(false); 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, 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); upShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::left); addShortcut(leftShortcut); CSMPrefs::Shortcut* downShortcut = new CSMPrefs::Shortcut("orbit-down", widget); downShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::right); addShortcut(rightShortcut); CSMPrefs::Shortcut* rollLeftShortcut = new CSMPrefs::Shortcut("orbit-roll-left", widget); rollLeftShortcut->enable(false); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::rollRight); addShortcut(rollRightShortcut); CSMPrefs::Shortcut* speedModeShortcut = new CSMPrefs::Shortcut("orbit-speed-mode", widget); speedModeShortcut->enable(false); connect(speedModeShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &OrbitCameraController::swapSpeedMode); addShortcut(speedModeShortcut); } osg::Vec3d OrbitCameraController::getCenter() const { return mCenter; } double OrbitCameraController::getOrbitSpeed() const { return mOrbitSpeed; } double OrbitCameraController::getOrbitSpeedMultiplier() const { return mOrbitSpeedMult; } unsigned int OrbitCameraController::getPickingMask() const { return mPickingMask; } void OrbitCameraController::setCenter(const osg::Vec3d& value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); mCenter = value; mDistance = (eye - mCenter).length(); getCamera()->setViewMatrixAsLookAt(eye, mCenter, up); mInitialized = true; } void OrbitCameraController::setOrbitSpeed(double value) { mOrbitSpeed = value; } void OrbitCameraController::setOrbitSpeedMultiplier(double value) { mOrbitSpeedMult = value; } void OrbitCameraController::setPickingMask(unsigned int value) { mPickingMask = value; } void OrbitCameraController::handleMouseMoveEvent(int x, int y) { if (!isActive()) return; if (!mInitialized) initialize(); if (mNaviPrimary) { double scalar = getCameraSensitivity() * (getInverted() ? -1.0 : 1.0); rotateHorizontal(x * scalar); rotateVertical(-y * scalar); } else if (mNaviSecondary) { osg::Vec3d movement; movement += LocalLeft * x * getSecondaryMovementMultiplier(); movement += LocalUp * -y * getSecondaryMovementMultiplier(); translate(movement); } } void OrbitCameraController::handleMouseScrollEvent(int x) { if (!isActive()) return; zoom(-x * ((mFast ^ mFastAlternate) ? getWheelMovementMultiplier() : 1)); } void OrbitCameraController::update(double dt) { if (!isActive()) return; if (!mInitialized) initialize(); double rotDist = mOrbitSpeed * dt; if (mFast ^ mFastAlternate) rotDist *= mOrbitSpeedMult; if (mLeft) rotateHorizontal(-rotDist); if (mRight) rotateHorizontal(rotDist); if (mUp) rotateVertical(rotDist); if (mDown) rotateVertical(-rotDist); if (mRollLeft) roll(-rotDist); if (mRollRight) roll(rotDist); // Normalize the matrix to counter drift getCamera()->getViewMatrix().orthoNormal(getCamera()->getViewMatrix()); } void OrbitCameraController::reset() { mInitialized = false; } void OrbitCameraController::initialize() { 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)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(mPickingMask); getCamera()->accept(visitor); osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up, DefaultStartDistance); if (intersector->getIntersections().begin() != intersector->getIntersections().end()) { mCenter = intersector->getIntersections().begin()->getWorldIntersectPoint(); mDistance = (eye - mCenter).length(); } else { mCenter = center; mDistance = DefaultStartDistance; } mInitialized = true; } void OrbitCameraController::setConstRoll(bool enabled) { mConstRoll = enabled; } void OrbitCameraController::rotateHorizontal(double value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d absoluteUp = osg::Vec3(0, 0, 1); osg::Quat rotation = osg::Quat(value, mConstRoll ? absoluteUp : up); osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d newOffset = rotation * oldOffset; if (mConstRoll) up = rotation * up; getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); } void OrbitCameraController::rotateVertical(double value) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d forward = center - eye; osg::Vec3d axis = up ^ forward; osg::Quat rotation = osg::Quat(value, axis); osg::Vec3d oldOffset = eye - mCenter; osg::Vec3d newOffset = rotation * oldOffset; if (mConstRoll) up = rotation * up; getCamera()->setViewMatrixAsLookAt(mCenter + newOffset, mCenter, up); } void OrbitCameraController::roll(double value) { getCamera()->getViewMatrix() *= osg::Matrixd::rotate(value, LocalForward); } void OrbitCameraController::translate(const osg::Vec3d& offset) { osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d newOffset = getCamera()->getViewMatrix().getRotate().inverse() * offset; mCenter += newOffset; eye += newOffset; getCamera()->setViewMatrixAsLookAt(eye, mCenter, up); } void OrbitCameraController::zoom(double value) { mDistance = std::max(10., mDistance + value); osg::Vec3d eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up, 1.f); osg::Vec3d offset = (eye - center) * mDistance; getCamera()->setViewMatrixAsLookAt(mCenter + offset, mCenter, up); } void OrbitCameraController::naviPrimary(bool active) { mNaviPrimary = active; } void OrbitCameraController::naviSecondary(bool active) { mNaviSecondary = active; } void OrbitCameraController::up(bool active) { mUp = active; } void OrbitCameraController::left(bool active) { mLeft = active; } void OrbitCameraController::down(bool active) { mDown = active; } void OrbitCameraController::right(bool active) { mRight = active; } void OrbitCameraController::rollLeft(bool active) { if (isActive()) mRollLeft = active; } void OrbitCameraController::rollRight(bool active) { mRollRight = active; } void OrbitCameraController::alternateFast(bool active) { mFastAlternate = active; } void OrbitCameraController::swapSpeedMode() { mFast = !mFast; } } openmw-openmw-0.49.0/apps/opencs/view/render/cameracontroller.hpp000066400000000000000000000115651503074453300251600ustar00rootroot00000000000000#ifndef OPENCS_VIEW_CAMERACONTROLLER_H #define OPENCS_VIEW_CAMERACONTROLLER_H #include #include #include namespace osg { class Camera; class Group; } namespace CSMPrefs { class Shortcut; } namespace CSVRender { 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; CameraController(QObject* parent); ~CameraController() override = default; bool isActive() const; osg::Camera* getCamera() const; double getCameraSensitivity() const; bool getInverted() const; double getSecondaryMovementMultiplier() const; double getWheelMovementMultiplier() const; void setCamera(osg::Camera*); void setCameraSensitivity(double value); void setInverted(bool value); void setSecondaryMovementMultiplier(double value); void setWheelMovementMultiplier(double value); // moves the camera to an intelligent position void setup(osg::Group* root, unsigned int mask, const osg::Vec3d& up); virtual void handleMouseMoveEvent(int x, int y) = 0; virtual void handleMouseScrollEvent(int x) = 0; virtual void update(double dt) = 0; protected: void addShortcut(CSMPrefs::Shortcut* shortcut); private: bool mActive, mInverted; double mCameraSensitivity; double mSecondaryMoveMult; double mWheelMoveMult; osg::Camera* mCamera; std::vector mShortcuts; }; class FreeCameraController : public CameraController { Q_OBJECT public: FreeCameraController(QWidget* parent); double getLinearSpeed() const; double getRotationalSpeed() const; double getSpeedMultiplier() const; void setLinearSpeed(double value); void setRotationalSpeed(double value); void setSpeedMultiplier(double value); void fixUpAxis(const osg::Vec3d& up); void unfixUpAxis(); void handleMouseMoveEvent(int x, int y) override; void handleMouseScrollEvent(int x) 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); void stabilize(); bool mLockUpright, mModified; bool mNaviPrimary, mNaviSecondary; bool mFast, mFastAlternate; bool mLeft, mRight, mForward, mBackward, mRollLeft, mRollRight; osg::Vec3d mUp; 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(); }; class OrbitCameraController : public CameraController { Q_OBJECT public: OrbitCameraController(QWidget* parent); osg::Vec3d getCenter() const; double getOrbitSpeed() const; double getOrbitSpeedMultiplier() const; unsigned int getPickingMask() const; void setCenter(const osg::Vec3d& center); void setOrbitSpeed(double value); void setOrbitSpeedMultiplier(double value); void setPickingMask(unsigned int value); void handleMouseMoveEvent(int x, int y) override; void handleMouseScrollEvent(int x) override; void update(double dt) override; /// \brief Flag controller to be re-initialized. void reset(); void setConstRoll(bool enable); 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); 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; bool mConstRoll; 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cell.cpp000066400000000000000000000471301503074453300225330ustar00rootroot00000000000000#include "cell.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/idtable.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 #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; } 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(); } }; } bool CSVRender::Cell::removeObject(const std::string& id) { std::map::iterator iter = mObjects.find(Misc::StringUtils::lowerCase(id)); if (iter == mObjects.end()) return false; removeObject(iter); return true; } std::map::iterator CSVRender::Cell::removeObject( std::map::iterator iter) { delete iter->second; mObjects.erase(iter++); return iter; } bool CSVRender::Cell::addObjects(int start, int end) { bool modified = false; const CSMWorld::RefCollection& collection = mData.getReferences(); for (int i = start; i <= end; ++i) { const auto& cellId = ESM::RefId::stringRefId(collection.getRecord(i).get().mCell.toString()); CSMWorld::RecordBase::State state = collection.getRecord(i).mState; if (cellId == mId && state != CSMWorld::RecordBase::State_Deleted) { const std::string& id = collection.getRecord(i).get().mId.getRefIdString(); auto object = std::make_unique(mData, mCellNode, id, false); if (mSubModeElementMask & Mask_Reference) object->setSubMode(mSubMode); mObjects.insert(std::make_pair(id, object.release())); modified = true; } } return modified; } void CSVRender::Cell::updateLand() { if (!mUpdateLand || mLandDeleted) return; mUpdateLand = false; // Cell is deleted if (mDeleted) { unloadLand(); return; } const CSMWorld::IdCollection& land = mData.getLand(); if (land.getRecord(mId).isDeleted()) return; const ESM::Land& esmLand = land.getRecord(mId).get(); 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); } mTerrain->loadCell(esmLand.mX, esmLand.mY); if (!mCellBorder) mCellBorder = std::make_unique(mCellNode, mCoordinates); mCellBorder->buildShape(esmLand); } void CSVRender::Cell::unloadLand() { if (mTerrain) mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); if (mCellBorder) mCellBorder.reset(); } 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); mTerrainStorage = new TerrainStorage(mData); if (result.second) mCoordinates = result.first; mCellNode = new osg::Group; mCellNode->setUserData(new CellNodeContainer(this)); mCellNode->setUpdateCallback(new CellNodeCallback); rootNode->addChild(mCellNode); setCellMarker(); if (!mDeleted) { CSMWorld::IdTable& references = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); int rows = references.rowCount(); addObjects(0, rows - 1); 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 = 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) delete iter->second; mCellNode->getParent(0)->removeChild(mCellNode); } CSVRender::Pathgrid* CSVRender::Cell::getPathgrid() const { return mPathgrid.get(); } 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)) modified = true; return modified; } 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)) modified = true; return modified; } 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)); 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) { auto cell = ESM::RefId::stringRefId(references.data(references.index(i, cellColumn)).toString().toUtf8().constData()); if (cell == mId) { std::string id = Misc::StringUtils::lowerCase( references.data(references.index(i, idColumn)).toString().toUtf8().constData()); int state = references.data(references.index(i, stateColumn)).toInt(); 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()) { if (iter->second->referenceDataChanged(topLeft, bottomRight)) modified = true; std::map::iterator iter2 = ids.find(iter->first); if (iter2 != ids.end()) { bool deleted = iter2->second; ids.erase(iter2); if (deleted) { iter = removeObject(iter); modified = true; continue; } } ++iter; } // add new objects 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))); modified = true; } } return modified; } bool CSVRender::Cell::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; if (mDeleted) return false; CSMWorld::IdTable& references = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_References)); 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())) modified = true; return modified; } bool CSVRender::Cell::referenceAdded(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return false; if (mDeleted) return false; return addObjects(start, end); } void CSVRender::Cell::setAlteredHeight(int inCellX, int inCellY, float height) { mTerrainStorage->setAlteredHeight(inCellX, inCellY, height); mUpdateLand = true; } float CSVRender::Cell::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { return mTerrainStorage->getSumOfAlteredAndTrueHeight(cellX, cellY, inCellX, inCellY); } float* CSVRender::Cell::getAlteredHeight(int inCellX, int inCellY) { return mTerrainStorage->getAlteredHeight(inCellX, inCellY); } void CSVRender::Cell::resetAlteredHeights() { mTerrainStorage->resetHeights(); mUpdateLand = true; } void CSVRender::Cell::pathgridModified() { if (mPathgrid) mPathgrid->recreateGeometry(); } void CSVRender::Cell::pathgridRemoved() { if (mPathgrid) mPathgrid->removeGeometry(); } void CSVRender::Cell::landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } 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) { mUpdateLand = true; mLandDeleted = false; } void CSVRender::Cell::landTextureChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { mUpdateLand = true; } void CSVRender::Cell::landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end) { mUpdateLand = true; } 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) { iter->second->reloadAssets(); } if (mTerrain) { mTerrain->unloadCell(mCoordinates.getX(), mCoordinates.getY()); mTerrain->loadCell(mCoordinates.getX(), mCoordinates.getY()); } if (mCellWater) mCellWater->reloadAssets(); } void CSVRender::Cell::setSelection(int elementMask, Selection mode) { if (elementMask & Mask_Reference) { 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; } iter->second->setSelected(selected); } } if (mPathgrid && elementMask & Mask_Pathgrid) { // Only one pathgrid may be selected, so some operations will only have an effect // if the pathgrid is already focused switch (mode) { case Selection_Clear: mPathgrid->clearSelected(); break; case Selection_All: if (mPathgrid->isSelected()) mPathgrid->selectAll(); break; case Selection_Invert: if (mPathgrid->isSelected()) mPathgrid->invertSelected(); break; } } } void CSVRender::Cell::selectAllWithSameParentId(int elementMask) { std::set ids; for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { if (iter->second->getSelected()) ids.insert(iter->second->getReferenceableId()); } for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) { if (!iter->second->getSelected() && ids.find(iter->second->getReferenceableId()) != ids.end()) { iter->second->setSelected(true); } } } void CSVRender::Cell::handleSelectDrag(Object* object, DragMode dragMode) { if (dragMode == DragMode_Select_Only || dragMode == DragMode_Select_Add) object->setSelected(true); else if (dragMode == DragMode_Select_Remove) object->setSelected(false); else if (dragMode == DragMode_Select_Invert) 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 ((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[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); } } } } void CSVRender::Cell::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) { for (auto& object : mObjects) { if (dragMode == DragMode_Select_Only) object.second->setSelected(false); float distanceFromObject = (point - object.second->getPosition().asVec3()).length(); if (distanceFromObject < distance) handleSelectDrag(object.second, dragMode); } } void CSVRender::Cell::setCellArrows(int mask) { for (int i = 0; i < 4; ++i) { CellArrow::Direction direction = static_cast(1 << i); bool enable = mask & direction; if (enable != (mCellArrows[i].get() != nullptr)) { if (enable) mCellArrows[i] = std::make_unique(mCellNode, direction, mCoordinates); else mCellArrows[i].reset(nullptr); } } } void CSVRender::Cell::setCellMarker() { bool cellExists = false; bool isInteriorCell = false; int cellIndex = mData.getCells().searchId(mId); if (cellIndex > -1) { const CSMWorld::Record& cellRecord = mData.getCells().getRecord(cellIndex); cellExists = !cellRecord.isDeleted(); isInteriorCell = cellRecord.get().mData.mFlags & ESM::Cell::Interior; } if (!isInteriorCell) { mCellMarker = std::make_unique(mCellNode, mCoordinates, cellExists); } } CSMWorld::CellCoordinates CSVRender::Cell::getCoordinates() const { return mCoordinates; } bool CSVRender::Cell::isDeleted() const { return mDeleted; } 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; if (elementMask & Mask_Reference) for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) if (iter->second->getSelected()) result.push_back(iter->second->getTag()); if (mPathgrid && elementMask & Mask_Pathgrid) if (mPathgrid->isSelected()) result.emplace_back(mPathgrid->getTag()); return result; } std::vector> CSVRender::Cell::getEdited(unsigned int elementMask) const { std::vector> result; if (elementMask & Mask_Reference) for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) if (iter->second->isEdited()) result.push_back(iter->second->getTag()); return result; } 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); } void CSVRender::Cell::reset(unsigned int elementMask) { if (elementMask & Mask_Reference) for (std::map::const_iterator iter(mObjects.begin()); iter != mObjects.end(); ++iter) iter->second->reset(); if (mPathgrid && elementMask & Mask_Pathgrid) mPathgrid->resetIndicators(); } openmw-openmw-0.49.0/apps/opencs/view/render/cell.hpp000066400000000000000000000133471503074453300225430ustar00rootroot00000000000000#ifndef OPENCS_VIEW_CELL_H #define OPENCS_VIEW_CELL_H #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/cellcoordinates.hpp" #include "instancedragmodes.hpp" #include #include class QModelIndex; namespace osg { class Group; } namespace CSMWorld { class Data; } namespace Terrain { class TerrainGrid; } namespace CSVRender { class CellWater; class Pathgrid; class TagBase; class TerrainStorage; class Object; class CellArrow; class CellBorder; class CellMarker; class Cell { 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); // 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); void updateLand(); void unloadLand(); public: enum Selection { Selection_Clear, Selection_All, Selection_Invert }; public: /// \note Deleted covers both cells that are deleted and cells that don't exist in /// the first place. Cell(CSMDoc::Document& document, osg::Group* rootNode, const std::string& id, bool deleted = false, bool isExterior = false); ~Cell(); /// \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 referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); /// \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 referenceDataChanged(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 referenceAdded(const QModelIndex& parent, int start, int end); void setAlteredHeight(int inCellX, int inCellY, float height); float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); float* getAlteredHeight(int inCellX, int inCellY); void resetAlteredHeights(); void pathgridModified(); void pathgridRemoved(); void landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void landAboutToBeRemoved(const QModelIndex& parent, int start, int end); void landAdded(const QModelIndex& parent, int start, int end); void landTextureChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void landTextureAboutToBeRemoved(const QModelIndex& parent, int start, int end); void landTextureAdded(const QModelIndex& parent, int start, int end); void reloadAssets(); void setSelection(int elementMask, Selection mode); // Select everything that references the same ID as at least one of the elements // already selected void selectAllWithSameParentId(int elementMask); void selectFromGroup(const std::vector& group); void unhideAll(); void handleSelectDrag(Object* object, 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 setCellArrows(int mask); /// \brief Set marker for this cell. void setCellMarker(); /// Returns 0, 0 in case of an unpaged cell. CSMWorld::CellCoordinates getCoordinates() const; bool isDeleted() const; osg::ref_ptr getSnapTarget(unsigned int elementMask) const; std::vector> getSelection(unsigned int elementMask) const; std::vector> getEdited(unsigned int elementMask) const; 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); friend class CellNodeCallback; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cellarrow.cpp000066400000000000000000000140721503074453300236050ustar00rootroot00000000000000#include "cellarrow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/prefs/state.hpp" #include #include #include #include #include "mask.hpp" namespace CSVRender { struct WorldspaceHitResult; } 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 WorldspaceHitResult& /*hit*/) const { 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; } if (!hideBasics) { text += "

" "Modify which cells are shown" "

  • {scene-edit-primary}: Add cell in given direction
  • " "
  • {scene-edit-secondary}: Add cell and remove old cell
  • " "
  • {scene-select-primary}: Add cells in given direction
  • " "
  • {scene-select-secondary}: Add cells and remove old cells
  • " "
  • {scene-load-cam-cell}: Load cell where camera is located
  • " "
  • {scene-load-cam-eastcell}: Load cell to east
  • " "
  • {scene-load-cam-northcell}: Load cell to north
  • " "
  • {scene-load-cam-westcell}: Load cell to west
  • " "
  • {scene-load-cam-southcell}: Load cell to south
  • " "
"; } return CSMPrefs::State::get().getShortcutManager().processToolTip(text); } void CSVRender::CellArrow::adjustTransform() { // position const int cellSize = Constants::CellSizeInUnits; const int offset = cellSize / 2 + 600; 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); 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; }; 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); } void CSVRender::CellArrow::buildShape() { osg::ref_ptr geometry(new osg::Geometry); 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) { 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); osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // top 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); // back 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); // 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(4 + 6); primitives->push_back(5 + 6); primitives->push_back(2 + 6); primitives->push_back(4 + 6); primitives->push_back(2 + 6); primitives->push_back(1 + 6); geometry->addPrimitiveSet(primitives); osg::Vec4Array* colours = new osg::Vec4Array; 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->setColorArray(colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mBaseNode->addChild(geometry); } 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)); mParentNode->addChild(mBaseNode); mBaseNode->setNodeMask(Mask_CellArrow); adjustTransform(); buildShape(); } CSVRender::CellArrow::~CellArrow() { mParentNode->removeChild(mBaseNode); } CSMWorld::CellCoordinates CSVRender::CellArrow::getCoordinates() const { return mCoordinates; } CSVRender::CellArrow::Direction CSVRender::CellArrow::getDirection() const { return mDirection; } openmw-openmw-0.49.0/apps/opencs/view/render/cellarrow.hpp000066400000000000000000000026341503074453300236130ustar00rootroot00000000000000#ifndef OPENCS_VIEW_CELLARROW_H #define OPENCS_VIEW_CELLARROW_H #include "tagbase.hpp" #include #include #include "../../model/world/cellcoordinates.hpp" namespace osg { class PositionAttitudeTransform; class Group; } namespace CSVRender { class CellArrow; struct WorldspaceHitResult; class CellArrowTag : public TagBase { CellArrow* mArrow; public: CellArrowTag(CellArrow* arrow); CellArrow* getCellArrow() const; 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&); Direction mDirection; osg::Group* mParentNode; osg::ref_ptr mBaseNode; CSMWorld::CellCoordinates mCoordinates; void adjustTransform(); void buildShape(); public: CellArrow(osg::Group* cellNode, Direction direction, const CSMWorld::CellCoordinates& coordinates); ~CellArrow(); CSMWorld::CellCoordinates getCoordinates() const; Direction getDirection() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cellborder.cpp000066400000000000000000000103131503074453300237220ustar00rootroot00000000000000#include "cellborder.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "mask.hpp" #include "../../model/world/cellcoordinates.hpp" const int CSVRender::CellBorder::CellSize = ESM::Land::REAL_SIZE; /* The number of vertices per cell border is equal to the number of vertices per edge minus the duplicated corner vertices. An additional vertex to close the loop is NOT needed. */ 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)); mBaseNode->addChild(mBorderGeometry); mParentNode->addChild(mBaseNode); } CSVRender::CellBorder::~CellBorder() { mParentNode->removeChild(mBaseNode); } void CSVRender::CellBorder::buildShape(const ESM::Land& esmLand) { const ESM::Land::LandData* landData = esmLand.getLandData(ESM::Land::DATA_VHGT); mBaseNode->removeChild(mBorderGeometry); mBorderGeometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(); int x = 0; int y = 0; /* 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. */ 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); osg::ref_ptr colors = new osg::Vec4Array(); colors->push_back(osg::Vec4f(0.f, 0.5f, 0.f, 1.f)); mBorderGeometry->setColorArray(colors, osg::Array::BIND_PER_PRIMITIVE_SET); 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) primitives->setElement(i, i); // Assign the last primitive to the first vertex to close the loop. primitives->setElement(VertexCount, 0); mBorderGeometry->addPrimitiveSet(primitives); mBorderGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mBaseNode->addChild(mBorderGeometry); } size_t CSVRender::CellBorder::landIndex(int x, int y) { return static_cast(y) * ESM::Land::LAND_SIZE + x; } float CSVRender::CellBorder::scaleToWorld(int value) { return (CellSize + 128) * (float)value / ESM::Land::LAND_SIZE; } openmw-openmw-0.49.0/apps/opencs/view/render/cellborder.hpp000066400000000000000000000016771503074453300237440ustar00rootroot00000000000000#ifndef OPENCS_VIEW_CELLBORDER_H #define OPENCS_VIEW_CELLBORDER_H #include #include namespace osg { class Geometry; class Group; class PositionAttitudeTransform; } namespace ESM { struct Land; } namespace CSMWorld { class CellCoordinates; } 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; size_t landIndex(int x, int y); float scaleToWorld(int val); // unimplemented CellBorder(const CellBorder&); CellBorder& operator=(const CellBorder&); osg::Group* mParentNode; osg::ref_ptr mBaseNode; osg::ref_ptr mBorderGeometry; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cellmarker.cpp000066400000000000000000000057751503074453300237460ustar00rootroot00000000000000#include "cellmarker.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include CSVRender::CellMarkerTag::CellMarkerTag(CellMarker* marker) : TagBase(Mask_CellMarker) , mMarker(marker) { } CSVRender::CellMarker* CSVRender::CellMarkerTag::getCellMarker() const { return mMarker; } void CSVRender::CellMarker::buildMarker() { const int characterSize = 20; // Set up attributes of marker text. osg::ref_ptr markerText(new osgText::Text); markerText->setLayout(osgText::Text::LEFT_TO_RIGHT); markerText->setCharacterSize(characterSize); markerText->setAlignment(osgText::Text::CENTER_CENTER); markerText->setDrawMode(osgText::Text::TEXT | osgText::Text::FILLEDBOUNDINGBOX); // If cell exists then show black bounding box otherwise show red. if (mExists) { markerText->setBoundingBoxColor(osg::Vec4f(0.0f, 0.0f, 0.0f, 1.0f)); } else { markerText->setBoundingBoxColor(osg::Vec4f(1.0f, 0.0f, 0.0f, 1.0f)); } // Add text containing cell's coordinates. std::string coordinatesText = std::to_string(mCoordinates.getX()) + "," + std::to_string(mCoordinates.getY()); markerText->setText(coordinatesText); // Add text to marker node. mMarkerNode->addChild(markerText); } void CSVRender::CellMarker::positionMarker() { const int cellSize = Constants::CellSizeInUnits; const int markerHeight = 0; // Move marker to center of cell. int x = (mCoordinates.getX() * cellSize) + (cellSize / 2); int y = (mCoordinates.getY() * cellSize) + (cellSize / 2); mMarkerNode->setPosition(osg::Vec3f(x, y, markerHeight)); } CSVRender::CellMarker::CellMarker( 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(); mMarkerNode->setAutoRotateMode(osg::AutoTransform::ROTATE_TO_SCREEN); mMarkerNode->setAutoScaleToScreen(true); mMarkerNode->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); mMarkerNode->getOrCreateStateSet()->setRenderBinDetails(11, "RenderBin"); osg::ref_ptr mat = new osg::Material; mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE); mMarkerNode->getOrCreateStateSet()->setAttribute(mat); mMarkerNode->setUserData(new CellMarkerTag(this)); mMarkerNode->setNodeMask(Mask_CellMarker); mCellNode->addChild(mMarkerNode); buildMarker(); positionMarker(); } CSVRender::CellMarker::~CellMarker() { mCellNode->removeChild(mMarkerNode); } openmw-openmw-0.49.0/apps/opencs/view/render/cellmarker.hpp000066400000000000000000000025341503074453300237410ustar00rootroot00000000000000#ifndef OPENCS_VIEW_CELLMARKER_H #define OPENCS_VIEW_CELLMARKER_H #include "tagbase.hpp" #include #include "../../model/world/cellcoordinates.hpp" namespace osg { class AutoTransform; class Group; } namespace CSVRender { class CellMarker; class CellMarkerTag : public TagBase { private: CellMarker* mMarker; public: CellMarkerTag(CellMarker* marker); 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/cellwater.cpp000066400000000000000000000134721503074453300236000ustar00rootroot00000000000000#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 "../../model/world/cell.hpp" #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/data.hpp" #include "mask.hpp" 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) : mData(data) , mId(id) , mParentNode(cellNode) , mWaterTransform(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->setNodeMask(Mask_Water); mParentNode->addChild(mWaterTransform); mWaterGroup = new osg::Group(); mWaterTransform->addChild(mWaterGroup); const int cellIndex = mData.getCells().searchId(ESM::RefId::stringRefId(mId)); if (cellIndex > -1) { updateCellData(mData.getCells().getRecord(cellIndex)); } // Keep water existence/height up to date QAbstractItemModel* cells = mData.getTableModel(CSMWorld::UniversalId::Type_Cells); connect(cells, &QAbstractItemModel::dataChanged, this, &CellWater::cellDataChanged); } CellWater::~CellWater() { mParentNode->removeChild(mWaterTransform); } void CellWater::updateCellData(const CSMWorld::Record& cellRecord) { mDeleted = cellRecord.isDeleted(); if (!mDeleted) { const CSMWorld::Cell& cell = cellRecord.get(); if (mExterior != cell.isExterior() || mHasWater != cell.hasWater()) { mExterior = cellRecord.get().isExterior(); mHasWater = cellRecord.get().hasWater(); recreate(); } float waterHeight = -1; if (!mExterior) { waterHeight = cellRecord.get().mWater; } osg::Vec3d pos = mWaterTransform->getPosition(); pos.z() = waterHeight; mWaterTransform->setPosition(pos); } else { recreate(); } } void CellWater::reloadAssets() { recreate(); } void CellWater::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::Collection& cells = mData.getCells(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Record& cellRecord = cells.getRecord(row); if (cellRecord.get().mId == ESM::RefId::stringRefId(mId)) updateCellData(cellRecord); } } void CellWater::recreate() { const int InteriorScalar = 20; const int SegmentsPerCell = 1; const int TextureRepeatsPerCell = 6; const float Alpha = 0.5f; const int RenderBin = osg::StateSet::TRANSPARENT_BIN - 1; if (mWaterGeometry) { mWaterGroup->removeChild(mWaterGeometry); mWaterGeometry = nullptr; } if (mDeleted || !mHasWater) return; float size; int segments; float textureRepeats; if (mExterior) { size = CellSize; segments = SegmentsPerCell; textureRepeats = TextureRepeatsPerCell; } else { size = CellSize * InteriorScalar; segments = SegmentsPerCell * InteriorScalar; textureRepeats = TextureRepeatsPerCell * InteriorScalar; } mWaterGeometry = SceneUtil::createWaterGeometry(size, segments, textureRepeats); mWaterGeometry->setStateSet(SceneUtil::createSimpleWaterStateSet(Alpha, RenderBin)); // Add water texture 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(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); mWaterGroup->addChild(mWaterGeometry); } } openmw-openmw-0.49.0/apps/opencs/view/render/cellwater.hpp000066400000000000000000000027461503074453300236070ustar00rootroot00000000000000#ifndef CSV_RENDER_CELLWATER_H #define CSV_RENDER_CELLWATER_H #include #include #include #include class QModelIndex; namespace osg { class Geometry; class Group; class PositionAttitudeTransform; } namespace CSMWorld { struct Cell; class CellCoordinates; class Data; template struct Record; } namespace CSVRender { /// For exterior cells, this adds a patch of water to fit the size of the cell. For interior cells with water, this /// 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); ~CellWater(); void updateCellData(const CSMWorld::Record& cellRecord); void reloadAssets(); private slots: void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); private: void recreate(); static const int CellSize; CSMWorld::Data& mData; std::string mId; osg::Group* mParentNode; osg::ref_ptr mWaterTransform; osg::ref_ptr mWaterGroup; osg::ref_ptr mWaterGeometry; bool mDeleted; bool mExterior; bool mHasWater; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/commands.cpp000066400000000000000000000020701503074453300234070ustar00rootroot00000000000000#include "commands.hpp" #include #include #include #include #include "terrainshapemode.hpp" #include "worldspacewidget.hpp" CSVRender::DrawTerrainSelectionCommand::DrawTerrainSelectionCommand( WorldspaceWidget* worldspaceWidget, QUndoCommand* parent) : mWorldspaceWidget(worldspaceWidget) { } void CSVRender::DrawTerrainSelectionCommand::redo() { tryUpdate(); } void CSVRender::DrawTerrainSelectionCommand::undo() { 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(); } openmw-openmw-0.49.0/apps/opencs/view/render/commands.hpp000066400000000000000000000022571503074453300234230ustar00rootroot00000000000000#ifndef CSV_RENDER_COMMANDS_HPP #define CSV_RENDER_COMMANDS_HPP #include #include namespace CSVRender { class WorldspaceWidget; /* Current solution to force a redrawing of the terrain-selection grid when undoing/redoing changes in the editor. This only triggers a simple redraw of the grid, so only use it in conjunction with actual data changes which deform the grid. Please note that this command needs to be put onto the QUndoStack twice: at the start and at the end of the related terrain manipulation. This makes sure that the grid is always updated after all changes have been undone or redone -- but it also means that the selection is redrawn once at the beginning of either action. Future refinement may solve that. */ class DrawTerrainSelectionCommand : public QUndoCommand { private: QPointer mWorldspaceWidget; public: DrawTerrainSelectionCommand(WorldspaceWidget* worldspaceWidget, QUndoCommand* parent = nullptr); void redo() override; void undo() override; void tryUpdate(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/editmode.cpp000066400000000000000000000044231503074453300234040ustar00rootroot00000000000000#include "editmode.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) { } unsigned int CSVRender::EditMode::getInteractionMask() const { return mMask; } void CSVRender::EditMode::activate(CSVWidget::SceneToolbar* toolbar) { mWorldspaceWidget->setInteractionMask(mMask); mWorldspaceWidget->clearSelection(~mMask); } void CSVRender::EditMode::setEditLock(bool locked) {} void CSVRender::EditMode::primaryOpenPressed(const WorldspaceHitResult& hit) {} void CSVRender::EditMode::primaryEditPressed(const WorldspaceHitResult& hit) {} void CSVRender::EditMode::secondaryEditPressed(const WorldspaceHitResult& hit) {} void CSVRender::EditMode::primarySelectPressed(const WorldspaceHitResult& hit) {} void CSVRender::EditMode::secondarySelectPressed(const WorldspaceHitResult& hit) {} void CSVRender::EditMode::tertiarySelectPressed(const WorldspaceHitResult& hit) {} bool CSVRender::EditMode::primaryEditStartDrag(const QPoint& pos) { return false; } bool CSVRender::EditMode::secondaryEditStartDrag(const QPoint& pos) { return false; } bool CSVRender::EditMode::primarySelectStartDrag(const QPoint& pos) { return false; } 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::dragCompleted(const QPoint& pos) {} void CSVRender::EditMode::dragAborted() {} void CSVRender::EditMode::dragWheel(int diff, double speedFactor) {} void CSVRender::EditMode::dragEnterEvent(QDragEnterEvent* event) {} void CSVRender::EditMode::dropEvent(QDropEvent* event) {} void CSVRender::EditMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::EditMode::mouseMoveEvent(QMouseEvent* event) {} int CSVRender::EditMode::getSubMode() const { return -1; } openmw-openmw-0.49.0/apps/opencs/view/render/editmode.hpp000066400000000000000000000064461503074453300234200ustar00rootroot00000000000000#ifndef CSV_RENDER_EDITMODE_H #define CSV_RENDER_EDITMODE_H #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 EditMode : public CSVWidget::ModeButton { Q_OBJECT WorldspaceWidget* mWorldspaceWidget; unsigned int mMask; protected: WorldspaceWidget& getWorldspaceWidget(); public: EditMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, unsigned int mask, const QString& tooltip = "", QWidget* parent = nullptr); unsigned int getInteractionMask() const; void activate(CSVWidget::SceneToolbar* toolbar) override; /// Default-implementation: Ignored. virtual void setEditLock(bool locked); /// Default-implementation: Ignored. virtual void primaryOpenPressed(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 primarySelectPressed(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 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 secondarySelectStartDrag(const QPoint& pos); /// 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 /// /// \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 void dragEnterEvent(QDragEnterEvent* event) override; /// Default-implementation: ignored void dropEvent(QDropEvent* event) override; /// Default-implementation: ignored void dragMoveEvent(QDragMoveEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; /// Default: return -1 virtual int getSubMode() const; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/instancedragmodes.hpp000066400000000000000000000006751503074453300253160ustar00rootroot00000000000000#ifndef CSV_WIDGET_INSTANCEDRAGMODES_H #define CSV_WIDGET_INSTANCEDRAGMODES_H namespace CSVRender { enum DragMode { DragMode_None, DragMode_Move, DragMode_Rotate, DragMode_Scale, DragMode_Select_Only, DragMode_Select_Add, DragMode_Select_Remove, DragMode_Select_Invert, DragMode_Move_Snap, DragMode_Rotate_Snap, DragMode_Scale_Snap }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/instancemode.cpp000066400000000000000000001444531503074453300242730ustar00rootroot00000000000000#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/tablemimedata.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "mask.hpp" #include "instancemovemode.hpp" #include "instanceselectionmode.hpp" #include "object.hpp" #include "pagedworldspacewidget.hpp" #include "worldspacewidget.hpp" int CSVRender::InstanceMode::getSubModeFromId(const std::string& id) const { return id == "move" ? 0 : (id == "rotate" ? 1 : 2); } osg::Vec3f CSVRender::InstanceMode::quatToEuler(const osg::Quat& rot) const { float x, y, z; float test = 2 * (rot.w() * rot.y() + rot.x() * rot.z()); if (std::abs(test) >= 1.f) { x = atan2(rot.x(), rot.w()); y = (test > 0) ? (osg::PI / 2) : (-osg::PI / 2); z = 0; } else { x = std::atan2(2 * (rot.w() * rot.x() - rot.y() * rot.z()), 1 - 2 * (rot.x() * rot.x() + rot.y() * rot.y())); y = std::asin(test); z = std::atan2(2 * (rot.w() * rot.z() - rot.x() * rot.y()), 1 - 2 * (rot.y() * rot.y() + rot.z() * rot.z())); } return osg::Vec3f(-x, -y, -z); } 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)); return zr * yr * xr; } 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) { 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]); ++objectCount; } } if (objectCount > 0) center /= objectCount; return center; } osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos) { osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); osg::Matrix windowMatrix = getWorldspaceWidget().getCamera()->getViewport()->computeWindowMatrix(); osg::Matrix combined = viewMatrix * projMatrix * windowMatrix; return pos * combined; } osg::Vec3f CSVRender::InstanceMode::getProjectionSpaceCoords(const osg::Vec3f& pos) { osg::Matrix viewMatrix = getWorldspaceWidget().getCamera()->getViewMatrix(); osg::Matrix projMatrix = getWorldspaceWidget().getCamera()->getProjectionMatrix(); osg::Matrix combined = viewMatrix * projMatrix; return pos * combined; } osg::Vec3f CSVRender::InstanceMode::getMousePlaneCoords(const QPoint& point, const osg::Vec3d& dragStart) { osg::Matrix viewMatrix; viewMatrix.invert(getWorldspaceWidget().getCamera()->getViewMatrix()); osg::Matrix projMatrix; projMatrix.invert(getWorldspaceWidget().getCamera()->getProjectionMatrix()); osg::Matrix combined = projMatrix * viewMatrix; /* calculate viewport normalized coordinates note: is there a reason to use getCamera()->getViewport()->computeWindowMatrix() instead? */ float x = (point.x() * 2) / getWorldspaceWidget().getCamera()->getViewport()->width() - 1.0f; float y = 1.0f - (point.y() * 2) / getWorldspaceWidget().getCamera()->getViewport()->height(); osg::Vec3f mousePlanePoint = osg::Vec3f(x, y, dragStart.z()) * combined; return mousePlanePoint; } 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; } QString CSVRender::InstanceMode::getTooltip() { return QString( "Instance editing" "
  • Use {scene-select-primary} and {scene-select-secondary} to select and unselect instances
  • " "
  • Use {scene-edit-primary} to manipulate instances
  • " "
  • Use {scene-select-tertiary} to select a reference object and then {scene-edit-secondary} to snap " "selection relative to the reference object
  • " "
  • Use {scene-submode-move}, {scene-submode-rotate}, {scene-submode-scale} to change to move, " "rotate, and " "scale modes respectively
  • " "
  • Use {scene-axis-x}, {scene-axis-y}, and {scene-axis-z} to lock changes to X, Y, and Z axes " "respectively
  • " "
  • Use {scene-delete} to delete currently selected objects
  • " "
  • Use {scene-duplicate} to duplicate instances
  • " "
  • Use {scene-instance-drop} to drop instances
"); } CSVRender::InstanceMode::InstanceMode( WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent) : EditMode(worldspaceWidget, Misc::ScalableIcon::load(":scenetoolbar/editing-instance"), Mask_Reference | Mask_Terrain, getTooltip(), parent) , mSubMode(nullptr) , mSubModeId("move") , mSelectionMode(nullptr) , mDragMode(DragMode_None) , mDragAxis(-1) , mLocked(false) , mUnitScaleDist(1) , mParentNode(std::move(parentNode)) { 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, 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); connect(new CSMPrefs::Shortcut("scene-instance-drop", worldspaceWidget), qOverload<>(&CSMPrefs::Shortcut::activated), this, &InstanceMode::dropToCollision); 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); }); } 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) { if (!mSubMode) { 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
  • " "
"); 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
  • " "
"); mSubMode->setButton(mSubModeId); connect(mSubMode, &CSVWidget::SceneToolMode::modeChanged, this, &InstanceMode::subModeChanged); } if (!mSelectionMode) mSelectionMode = new InstanceSelectionMode(toolbar, getWorldspaceWidget(), mParentNode); mDragMode = DragMode_None; EditMode::activate(toolbar); toolbar->addTool(mSubMode); toolbar->addTool(mSelectionMode); std::string subMode = mSubMode->getCurrentId(); getWorldspaceWidget().setSubMode(getSubModeFromId(subMode), Mask_Reference); } void CSVRender::InstanceMode::deactivate(CSVWidget::SceneToolbar* toolbar) { mDragMode = DragMode_None; getWorldspaceWidget().reset(Mask_Reference); if (mSelectionMode) { toolbar->removeTool(mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } if (mSubMode) { toolbar->removeTool(mSubMode); delete mSubMode; mSubMode = nullptr; } EditMode::deactivate(toolbar); } void CSVRender::InstanceMode::setEditLock(bool locked) { mLocked = locked; if (mLocked) getWorldspaceWidget().abortDrag(); } void CSVRender::InstanceMode::primaryEditPressed(const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) primarySelectPressed(hit); } void CSVRender::InstanceMode::primaryOpenPressed(const WorldspaceHitResult& hit) { if (hit.tag) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { const std::string refId = objectTag->mObject->getReferenceId(); emit requestFocus(refId); } } } void CSVRender::InstanceMode::secondaryEditPressed(const WorldspaceHitResult& hit) { if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) secondarySelectPressed(hit); } void CSVRender::InstanceMode::primarySelectPressed(const WorldspaceHitResult& hit) { getWorldspaceWidget().clearSelection(Mask_Reference); if (hit.tag) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(hit.tag.get())) { // hit an Object, select it CSVRender::Object* object = objectTag->mObject; 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::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())) { // hit an Object, toggle its selection state CSVRender::Object* object = objectTag->mObject; object->setSnapTarget(!object->getSnapTarget()); return; } } } bool CSVRender::InstanceMode::primaryEditStartDrag(const QPoint& pos) { if (mDragMode != DragMode_None || mLocked) 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; } else if (mSubModeId == "rotate") { objectTag->mObject->setEdited(Object::Override_Rotation); mDragMode = DragMode_Rotate; } else if (mSubModeId == "scale") { objectTag->mObject->setEdited(Object::Override_Scale); mDragMode = DragMode_Scale; // 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::secondaryEditStartDrag(const QPoint& pos) { if (mDragMode != DragMode_None || mLocked) 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) { 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; WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mSelectionMode->setDragStart(hit.worldPos); return true; } bool CSVRender::InstanceMode::secondarySelectStartDrag(const QPoint& pos) { 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; 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) { osg::Vec3f offset; osg::Quat rotation; std::vector> selection = getWorldspaceWidget().getEdited(Mask_Reference); auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { } else if (mDragMode == DragMode_Rotate || mDragMode == DragMode_Rotate_Snap) { osg::Vec3f eye, centre, up; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt(eye, centre, up); float angle; osg::Vec3f axis; if (mDragAxis == -1) { // Free rotate float rotationFactor = CSMPrefs::get()["3D Scene Input"]["rotate-factor"].toDouble() * speedFactor; osg::Quat cameraRotation = getWorldspaceWidget().getCamera()->getInverseViewMatrix().getRotate(); osg::Vec3f camForward = centre - eye; osg::Vec3f screenDir = cameraRotation * osg::Vec3f(diffX, diffY, 0); screenDir.normalize(); angle = std::sqrt(diffX * diffX + diffY * diffY) * rotationFactor; axis = screenDir ^ camForward; } else { // Global axis rotation osg::Vec3f camBack = eye - centre; for (int i = 0; i < 3; ++i) { if (i == mDragAxis) axis[i] = 1; else axis[i] = 0; } // Flip axis if facing opposite side if (camBack * axis < 0) axis *= -1; // Convert coordinate system osg::Vec3f screenCenter = getScreenCoords(getSelectionCenter(selection)); int widgetHeight = getWorldspaceWidget().height(); float newX = pos.x() - screenCenter.x(); float newY = (widgetHeight - pos.y()) - screenCenter.y(); float oldX = newX - diffX; float oldY = newY - diffY; // diffY appears to already be flipped osg::Vec3f oldVec = osg::Vec3f(oldX, oldY, 0); oldVec.normalize(); osg::Vec3f newVec = osg::Vec3f(newX, newY, 0); newVec.normalize(); // Find angle and axis of rotation angle = std::acos(oldVec * newVec) * speedFactor; if (((oldVec ^ newVec) * camBack < 0) ^ (camBack.z() < 0)) angle *= -1; } rotation = osg::Quat(angle, axis); } else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { osg::Vec3f center = getScreenCoords(getSelectionCenter(selection)); // Calculate scaling distance/rate int widgetHeight = getWorldspaceWidget().height(); float dx = pos.x() - center.x(); float dy = (widgetHeight - pos.y()) - center.y(); float dist = std::sqrt(dx * dx + dy * dy); float scale = dist / mUnitScaleDist; // Only uniform scaling is currently supported offset = osg::Vec3f(scale, scale, scale); } else if (mSelectionMode->getCurrentId() == "cube-centre") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionCubeCentre(mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "cube-corner") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionCubeCorner(mousePlanePoint); return; } else if (mSelectionMode->getCurrentId() == "sphere") { osg::Vec3f mousePlanePoint = getMousePlaneCoords(pos, getProjectionSpaceCoords(mSelectionMode->getDragStart())); mSelectionMode->drawSelectionSphere(mousePlanePoint); return; } int i = 0; // Apply for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter, i++) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { ESM::Position position = objectTag->mObject->getPosition(); 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) { 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 || mDragMode == DragMode_Rotate_Snap) { ESM::Position position = objectTag->mObject->getPosition(); osg::Quat currentRot = eulerToQuat(osg::Vec3f(position.rot[0], position.rot[1], position.rot[2])); osg::Quat combined = currentRot * rotation; osg::Vec3f euler = quatToEuler(combined); // There appears to be a very rare rounding error that can cause asin to return NaN if (!euler.isNaN()) { position.rot[0] = euler.x(); position.rot[1] = euler.y(); position.rot[2] = euler.z(); } objectTag->mObject->setRotation(position.rot); } else if (mDragMode == DragMode_Scale || mDragMode == DragMode_Scale_Snap) { // Reset scale objectTag->mObject->setEdited(0); objectTag->mObject->setEdited(Object::Override_Scale); float scale = objectTag->mObject->getScale(); scale *= offset.x(); if (mDragMode == DragMode_Scale_Snap) { scale = CSVRender::InstanceMode::roundFloatToMult( scale, CSMPrefs::get()["3D Scene Editing"]["gridsnap-scale"].toDouble()); } objectTag->mObject->setScale(scale); } } } } void CSVRender::InstanceMode::dragCompleted(const QPoint& pos) { std::vector> selection = getWorldspaceWidget().getEdited(Mask_Reference); auto* snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description; 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: handleSelectDrag(pos); return; break; case DragMode_Select_Add: handleSelectDrag(pos); return; break; case DragMode_Select_Remove: handleSelectDrag(pos); return; break; case DragMode_Select_Invert: handleSelectDrag(pos); return; 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); // 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 (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); mDragMode = DragMode_None; } void CSVRender::InstanceMode::dragWheel(int diff, double speedFactor) { if (mDragMode == DragMode_Move || mDragMode == DragMode_Move_Snap) { osg::Vec3f eye; osg::Vec3f centre; osg::Vec3f 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); auto snapTarget = dynamic_cast(getWorldspaceWidget().getSnapTarget(Mask_Reference).get()); int j = 0; for (std::vector>::iterator iter(selection.begin()); iter != selection.end(); ++iter, j++) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(iter->get())) { ESM::Position position = objectTag->mObject->getPosition(); auto preMovedObjectPosition = position.asVec3(); for (int i = 0; i < 3; ++i) position.pos[i] += offset[i]; 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) { if (const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData())) { if (!mime->fromDocument(getWorldspaceWidget().getDocument())) return; if (mime->holdsType(CSMWorld::UniversalId::Type_Referenceable)) event->accept(); } } void CSVRender::InstanceMode::dropEvent(QDropEvent* event) { if (const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData())) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); if (!mime->fromDocument(document)) return; WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getWorldspaceWidget().getInteractionMask()); std::string cellId = getWorldspaceWidget().getCellId(hit.worldPos); CSMWorld::IdTree& cellTable = dynamic_cast(*document.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); 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") return; if (mode == "Create cell and insert") { 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())) { CSMWorld::CellSelection selection = paged->getCellSelection(); selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); paged->setCellSelection(selection); } } } else if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); 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(); if (mode == "Discard") return; if (mode == "Show cell and insert") { selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); paged->setCellSelection(selection); } } } 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())) { // 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()); dropped = true; } if (dropped) event->accept(); } } int CSVRender::InstanceMode::getSubMode() const { return mSubMode ? getSubModeFromId(mSubMode->getCurrentId()) : 0; } void CSVRender::InstanceMode::subModeChanged(const std::string& id) { mSubModeId = id; getWorldspaceWidget().abortDrag(); 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); mDragMode = DragMode_None; } void CSVRender::InstanceMode::deleteSelectedInstances() { 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, "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); } 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) { object->setEdited(Object::Override_Position); ESM::Position position = object->getPosition(); position.pos[2] -= dropHeight; object->setPosition(position.pos); } float CSVRender::InstanceMode::calculateDropHeight(CSVRender::Object* object, float objectHeight) { osg::Vec3d point = object->getPosition().asVec3(); osg::Vec3d start = point; start.z() += objectHeight; osg::Vec3d end = point; end.z() = std::numeric_limits::lowest(); osg::ref_ptr intersector( new osgUtil::LineSegmentIntersector(osgUtil::Intersector::MODEL, start, end)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(Mask_Terrain | Mask_Reference); mParentNode->accept(visitor); osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); if (it != intersector->getIntersections().end()) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; float collisionLevel = intersection.getWorldIntersectPoint().z(); return point.z() - collisionLevel + objectHeight; } return 0.0f; } void CSVRender::InstanceMode::dropToCollision() { std::vector> selection = getWorldspaceWidget().getSelection(Mask_Reference); if (selection.empty()) return; CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::CommandMacro macro(undoStack, "Drop objects to collision"); DropObjectHeightHandler dropObjectDataHandler(&getWorldspaceWidget()); int counter = 0; for (osg::ref_ptr tag : selection) if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { float objectHeight = dropObjectDataHandler.mObjectHeights[counter++]; float dropHeight = calculateDropHeight(objectTag->mObject, objectHeight); dropInstance(objectTag->mObject, dropHeight); objectTag->mObject->apply(macro); } } CSVRender::DropObjectHeightHandler::DropObjectHeightHandler(WorldspaceWidget* worldspacewidget) : mWorldspaceWidget(worldspacewidget) { std::vector> selection = mWorldspaceWidget->getSelection(Mask_Reference); for (osg::ref_ptr tag : selection) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); osg::ref_ptr objectNodeWithoutGUI = objectTag->mObject->getBaseNode(); osg::ComputeBoundsVisitor computeBounds; computeBounds.setTraversalMask(Mask_Reference); objectNodeWithoutGUI->accept(computeBounds); osg::BoundingBox bounds = computeBounds.getBoundingBox(); float boundingBoxOffset = 0.0f; if (bounds.valid()) boundingBoxOffset = bounds.zMin(); mObjectHeights.emplace_back(boundingBoxOffset); mOldMasks.emplace_back(objectNodeWithGUI->getNodeMask()); objectNodeWithGUI->setNodeMask(0); } } } CSVRender::DropObjectHeightHandler::~DropObjectHeightHandler() { std::vector> selection = mWorldspaceWidget->getSelection(Mask_Reference); int counter = 0; for (osg::ref_ptr tag : selection) { if (CSVRender::ObjectTag* objectTag = dynamic_cast(tag.get())) { osg::ref_ptr objectNodeWithGUI = objectTag->mObject->getRootNode(); objectNodeWithGUI->setNodeMask(mOldMasks[counter]); counter++; } } } openmw-openmw-0.49.0/apps/opencs/view/render/instancemode.hpp000066400000000000000000000106471503074453300242750ustar00rootroot00000000000000#ifndef CSV_RENDER_INSTANCEMODE_H #define CSV_RENDER_INSTANCEMODE_H #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 { class TagBase; class InstanceSelectionMode; class Object; class WorldspaceWidget; struct WorldspaceHitResult; class InstanceMode : public EditMode { Q_OBJECT 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; QString getTooltip(); int getSubModeFromId(const std::string& id) const; osg::Vec3 quatToEuler(const osg::Quat& quat) const; osg::Quat eulerToQuat(const osg::Vec3& euler) const; float roundFloatToMult(const float val, const double mult) const; 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(CSVRender::Object* object, float objectHeight); osg::Vec3 calculateSnapPositionRelativeToTarget(osg::Vec3 initalPosition, osg::Vec3 targetPosition, osg::Vec3 targetRotation, osg::Vec3 translation, double snap) const; public: InstanceMode( WorldspaceWidget* worldspaceWidget, osg::ref_ptr parentNode, QWidget* parent = nullptr); void activate(CSVWidget::SceneToolbar* toolbar) override; void deactivate(CSVWidget::SceneToolbar* toolbar) override; void setEditLock(bool locked) override; void primaryOpenPressed(const WorldspaceHitResult& hit) override; void primaryEditPressed(const WorldspaceHitResult& hit) override; void secondaryEditPressed(const WorldspaceHitResult& hit) override; void primarySelectPressed(const WorldspaceHitResult& hit) override; void secondarySelectPressed(const WorldspaceHitResult& hit) override; void tertiarySelectPressed(const WorldspaceHitResult& hit) override; bool primaryEditStartDrag(const QPoint& pos) override; bool secondaryEditStartDrag(const QPoint& pos) override; bool primarySelectStartDrag(const QPoint& pos) override; bool secondarySelectStartDrag(const QPoint& pos) override; void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) 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; void dragWheel(int diff, double speedFactor) override; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; int getSubMode() const override; signals: void requestFocus(const std::string& id); private slots: 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 dropToCollision(); }; /// \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; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/instancemovemode.cpp000066400000000000000000000010561503074453300251510ustar00rootroot00000000000000#include "instancemovemode.hpp" #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) { } openmw-openmw-0.49.0/apps/opencs/view/render/instancemovemode.hpp000066400000000000000000000004541503074453300251570ustar00rootroot00000000000000#ifndef CSV_RENDER_INSTANCEMOVEMODE_H #define CSV_RENDER_INSTANCEMOVEMODE_H #include "../widget/modebutton.hpp" namespace CSVRender { class InstanceMoveMode : public CSVWidget::ModeButton { Q_OBJECT public: InstanceMoveMode(QWidget* parent = nullptr); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/instanceselectionmode.cpp000066400000000000000000000373311503074453300261750ustar00rootroot00000000000000#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 "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "instancedragmodes.hpp" #include "object.hpp" #include "worldspacewidget.hpp" namespace CSVWidget { class SceneToolbar; } namespace CSVRender { 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, &QAction::triggered, this, &InstanceSelectionMode::selectSame); connect(mDeleteSelection, &QAction::triggered, this, &InstanceSelectionMode::deleteSelection); } InstanceSelectionMode::~InstanceSelectionMode() { if (mBaseNode) mParentNode->removeChild(mBaseNode); } void InstanceSelectionMode::setDragStart(const osg::Vec3d& dragStart) { mDragStart = dragStart; } const osg::Vec3d& InstanceSelectionMode::getDragStart() { return mDragStart; } void InstanceSelectionMode::dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode) { float dragDistance = (mDragStart - dragEndPoint).length(); if (mBaseNode) mParentNode->removeChild(mBaseNode); if (getCurrentId() == "cube-centre") { osg::Vec3d pointA(mDragStart[0] - dragDistance, mDragStart[1] - dragDistance, mDragStart[2] - dragDistance); osg::Vec3d pointB(mDragStart[0] + dragDistance, mDragStart[1] + dragDistance, mDragStart[2] + dragDistance); getWorldspaceWidget().selectInsideCube(pointA, pointB, dragMode); } else if (getCurrentId() == "cube-corner") { getWorldspaceWidget().selectInsideCube(mDragStart, dragEndPoint, dragMode); } else if (getCurrentId() == "sphere") { getWorldspaceWidget().selectWithinDistance(mDragStart, dragDistance, dragMode); } } void InstanceSelectionMode::drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint) { float dragDistance = (mDragStart - mousePlanePoint).length(); drawSelectionCube(mDragStart, dragDistance); } void InstanceSelectionMode::drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint) { drawSelectionBox(mDragStart, mousePlanePoint); } void InstanceSelectionMode::drawSelectionBox(const osg::Vec3d& pointA, const osg::Vec3d& pointB) { if (mBaseNode) mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(pointA); 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])); 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); 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(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(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(4); primitives->push_back(1); primitives->push_back(5); 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(3); primitives->push_back(2); 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(5); primitives->push_back(3); primitives->push_back(7); geometry->addPrimitiveSet(primitives); 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)); 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_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } void InstanceSelectionMode::drawSelectionCube(const osg::Vec3d& point, float radius) { if (mBaseNode) mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); osg::ref_ptr geometry(new osg::Geometry); 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)); } geometry->setVertexArray(vertices); 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(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(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(4); primitives->push_back(1); primitives->push_back(5); 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(3); primitives->push_back(2); 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(5); primitives->push_back(3); primitives->push_back(7); geometry->addPrimitiveSet(primitives); 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)); 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_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3f& mousePlanePoint) { float dragDistance = (mDragStart - mousePlanePoint).length(); drawSelectionSphere(mDragStart, dragDistance); } void InstanceSelectionMode::drawSelectionSphere(const osg::Vec3d& point, float radius) { if (mBaseNode) mParentNode->removeChild(mBaseNode); mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->setPosition(point); osg::ref_ptr geometry(new osg::Geometry); 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; 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); // 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); for (int j = 0; j < resolution; ++j) { float vertexX = thisRadius * sin(j * reciprocalResolution * osg::PI * 2); 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)); 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)); } } geometry->setVertexArray(vertices); osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, 0); for (int i = 0; i < resolution; ++i) { // 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 > static_cast(vertices->size()) - 1) continue; primitives->push_back(i * resolution * 2); primitives->push_back(i * resolution * 2 + 1); // 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 + 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->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_CULL_FACE, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); mBaseNode->addChild(geometry); mParentNode->addChild(mBaseNode); } bool InstanceSelectionMode::createContextMenu(QMenu* menu) { if (menu) { SelectionMode::createContextMenu(menu); menu->addAction(mSelectSame); menu->addAction(mDeleteSelection); } return true; } void InstanceSelectionMode::selectSame() { getWorldspaceWidget().selectAllWithSameParentId(Mask_Reference); } void InstanceSelectionMode::deleteSelection() { 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) { CSMWorld::DeleteCommand* command = new CSMWorld::DeleteCommand( referencesTable, static_cast(iter->get())->mObject->getReferenceId()); getWorldspaceWidget().getDocument().getUndoStack().push(command); } } } openmw-openmw-0.49.0/apps/opencs/view/render/instanceselectionmode.hpp000066400000000000000000000043271503074453300262010ustar00rootroot00000000000000#ifndef CSV_RENDER_INSTANCE_SELECTION_MODE_H #define CSV_RENDER_INSTANCE_SELECTION_MODE_H #include #include class QAction; class QMenu; class QObject; class QPoint; namespace CSVWidget { class SceneToolbar; } namespace osg { class PositionAttitudeTransform; class Group; class Vec3f; } #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); ~InstanceSelectionMode(); /// Store the worldspace-coordinate when drag begins void setDragStart(const osg::Vec3d& dragStart); /// Store the worldspace-coordinate when drag begins const osg::Vec3d& getDragStart(); /// Store the screen-coordinate when drag begins void setScreenDragStart(const QPoint& dragStartPoint); /// Apply instance selection changes void dragEnded(const osg::Vec3d& dragEndPoint, DragMode dragMode); void drawSelectionCubeCentre(const osg::Vec3f& mousePlanePoint); void drawSelectionCubeCorner(const osg::Vec3f& mousePlanePoint); void drawSelectionSphere(const osg::Vec3f& mousePlanePoint); 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; 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; private slots: void deleteSelection(); void selectSame(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/lighting.cpp000066400000000000000000000032771503074453300234250ustar00rootroot00000000000000#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 { 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); } private: int mIndex; }; void CSVRender::Lighting::updateDayNightMode(int index) { if (mRootNode == nullptr) return; DayNightSwitchVisitor visitor(index); mRootNode->accept(visitor); } openmw-openmw-0.49.0/apps/opencs/view/render/lighting.hpp000066400000000000000000000012601503074453300234200ustar00rootroot00000000000000#ifndef OPENCS_VIEW_LIGHTING_H #define OPENCS_VIEW_LIGHTING_H #include namespace osg { class Vec4f; class LightSource; class Group; } namespace CSVRender { class Lighting { public: Lighting() : mRootNode(nullptr) { } virtual ~Lighting() = default; virtual void activate(osg::Group* rootNode, bool isExterior) = 0; virtual void deactivate() = 0; virtual osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) = 0; protected: void updateDayNightMode(int index); osg::ref_ptr mLightSource; osg::Group* mRootNode; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/lightingbright.cpp000066400000000000000000000017751503074453300246260ustar00rootroot00000000000000#include "lightingbright.hpp" #include #include #include #include CSVRender::LightingBright::LightingBright() {} void CSVRender::LightingBright::activate(osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = (new osg::LightSource); 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)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(0); } void CSVRender::LightingBright::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingBright::getAmbientColour(osg::Vec4f* /*defaultAmbient*/) { return osg::Vec4f(1.f, 1.f, 1.f, 1.f); } openmw-openmw-0.49.0/apps/opencs/view/render/lightingbright.hpp000066400000000000000000000007321503074453300246230ustar00rootroot00000000000000#ifndef OPENCS_VIEW_LIGHTING_BRIGHT_H #define OPENCS_VIEW_LIGHTING_BRIGHT_H #include "lighting.hpp" #include namespace osg { class Group; } namespace CSVRender { class LightingBright : public Lighting { public: LightingBright(); void activate(osg::Group* rootNode, bool /*isExterior*/) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/lightingday.cpp000066400000000000000000000020021503074453300241040ustar00rootroot00000000000000#include "lightingday.hpp" #include #include #include #include void CSVRender::LightingDay::activate(osg::Group* rootNode, bool /*isExterior*/) { mRootNode = rootNode; mLightSource = new osg::LightSource; 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)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(0); } void CSVRender::LightingDay::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingDay::getAmbientColour(osg::Vec4f* defaultAmbient) { if (defaultAmbient) return *defaultAmbient; else return osg::Vec4f(0.7f, 0.7f, 0.7f, 1.f); } openmw-openmw-0.49.0/apps/opencs/view/render/lightingday.hpp000066400000000000000000000007301503074453300241170ustar00rootroot00000000000000#ifndef OPENCS_VIEW_LIGHTING_DAY_H #define OPENCS_VIEW_LIGHTING_DAY_H #include "lighting.hpp" #include namespace osg { class Group; } namespace CSVRender { class LightingDay : public Lighting { public: LightingDay() = default; void activate(osg::Group* rootNode, bool /*isExterior*/) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/lightingnight.cpp000066400000000000000000000020331503074453300244440ustar00rootroot00000000000000#include "lightingnight.hpp" #include #include #include #include void CSVRender::LightingNight::activate(osg::Group* rootNode, bool isExterior) { mRootNode = rootNode; mLightSource = new osg::LightSource; 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)); light->setSpecular(osg::Vec4f(0.f, 0.f, 0.f, 0.f)); light->setConstantAttenuation(1.f); mLightSource->setLight(light); mRootNode->addChild(mLightSource); updateDayNightMode(isExterior ? 1 : 0); } void CSVRender::LightingNight::deactivate() { if (mRootNode && mLightSource.get()) mRootNode->removeChild(mLightSource); } osg::Vec4f CSVRender::LightingNight::getAmbientColour(osg::Vec4f* defaultAmbient) { if (defaultAmbient) return *defaultAmbient; else return osg::Vec4f(0.2f, 0.2f, 0.2f, 1.f); } openmw-openmw-0.49.0/apps/opencs/view/render/lightingnight.hpp000066400000000000000000000007331503074453300244560ustar00rootroot00000000000000#ifndef OPENCS_VIEW_LIGHTING_NIGHT_H #define OPENCS_VIEW_LIGHTING_NIGHT_H #include "lighting.hpp" #include namespace osg { class Group; } namespace CSVRender { class LightingNight : public Lighting { public: LightingNight() = default; void activate(osg::Group* rootNode, bool isExterior) override; void deactivate() override; osg::Vec4f getAmbientColour(osg::Vec4f* defaultAmbient) override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/mask.hpp000066400000000000000000000014271503074453300225530ustar00rootroot00000000000000#ifndef CSV_RENDER_ELEMENTS_H #define CSV_RENDER_ELEMENTS_H namespace CSVRender { /// Node masks used on the OSG scene graph in OpenMW-CS. /// @note See the respective file in OpenMW (apps/openmw/mwrender/vismask.hpp) /// for general usage hints about node masks. /// @copydoc MWRender::VisMask enum Mask : unsigned int { // elements that are part of the actual scene Mask_Hidden = 0x0, Mask_Reference = 0x1, Mask_Pathgrid = 0x2, Mask_Water = 0x4, Mask_Terrain = 0x8, // used within models Mask_ParticleSystem = 0x100, Mask_Lighting = 0x200, // control elements Mask_CellMarker = 0x10000, Mask_CellArrow = 0x20000, Mask_CellBorder = 0x40000 }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/object.cpp000066400000000000000000000562221503074453300230640ustar00rootroot00000000000000#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 "../../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 "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 shape(new osg::Box(osg::Vec3f(0, 0, 0), 50.f)); osg::ref_ptr shapedrawable(new osg::ShapeDrawable); shapedrawable->setShape(shape); 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 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() {} 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); int index = referenceables.searchId(mReferenceableId); const ESM::Light* light = nullptr; mBaseNode->removeChildren(0, mBaseNode->getNumChildren()); if (index == -1) { mBaseNode->addChild(createErrorCube()); return; } /// \todo check for Deleted state (error 1) int recordType = referenceables.getData(index, TypeIndex).toInt(); std::string model = referenceables.getData(index, ModelIndex).toString().toUtf8().constData(); if (recordType == CSMWorld::UniversalId::Type_Light) { light = &dynamic_cast&>(referenceables.getRecord(index)).get(); if (model.empty()) model = "marker_light.nif"; } if (recordType == CSMWorld::UniversalId::Type_CreatureLevelledList) { if (model.empty()) model = "marker_creature.nif"; } try { if (recordType == CSMWorld::UniversalId::Type_Npc || recordType == CSMWorld::UniversalId::Type_Creature) { if (!mActor) mActor = std::make_unique(mReferenceableId, mData); mActor->update(); mBaseNode->addChild(mActor->getBaseNode()); } else if (!model.empty()) { constexpr VFS::Path::NormalizedView meshes("meshes"); VFS::Path::Normalized path(meshes); path /= model; mResourceSystem->getSceneManager()->getInstance(path, mBaseNode); } else { throw std::runtime_error(mReferenceableId.getRefIdString() + " has no model"); } } catch (std::exception& e) { // TODO: use error marker mesh Log(Debug::Error) << e.what(); } if (light) { bool isExterior = false; // FIXME SceneUtil::addLight(mBaseNode, SceneUtil::LightCommon(*light), Mask_Lighting, isExterior); } } void CSVRender::Object::adjustTransform() { if (mReferenceId.empty()) return; ESM::Position position = getPosition(); // position 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); float scale = getScale(); mBaseNode->setScale(osg::Vec3(scale, scale, scale)); } const CSMWorld::CellRef& CSVRender::Object::getReference() const { if (mReferenceId.empty()) throw std::logic_error("object does not represent a reference"); return mData.getReferences().getRecord(mReferenceId).get(); } void CSVRender::Object::updateMarker() { for (int i = 0; i < 3; ++i) { if (mMarker[i]) { mRootNode->removeChild(mMarker[i]); mMarker[i] = osg::ref_ptr(); } if (mSelected) { if (mSubMode == 0) { mMarker[i] = makeMoveOrScaleMarker(i); mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); mRootNode->addChild(mMarker[i]); } else if (mSubMode == 1) { mMarker[i] = makeRotateMarker(i); mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); mRootNode->addChild(mMarker[i]); } else if (mSubMode == 2) { mMarker[i] = makeMoveOrScaleMarker(i); mMarker[i]->setUserData(new ObjectMarkerTag(this, i)); mRootNode->addChild(mMarker[i]); } } } } osg::ref_ptr CSVRender::Object::makeMoveOrScaleMarker(int axis) { osg::ref_ptr geometry(new osg::Geometry); float shaftLength = MarkerShaftBaseLength + mBaseNode->getBound().radius(); // shaft osg::Vec3Array* vertices = new osg::Vec3Array; for (int i = 0; i < 2; ++i) { float length = i ? shaftLength : MarkerShaftWidth; vertices->push_back(getMarkerPosition(-MarkerShaftWidth / 2, -MarkerShaftWidth / 2, length, axis)); vertices->push_back(getMarkerPosition(-MarkerShaftWidth / 2, MarkerShaftWidth / 2, length, axis)); vertices->push_back(getMarkerPosition(MarkerShaftWidth / 2, MarkerShaftWidth / 2, length, axis)); vertices->push_back(getMarkerPosition(MarkerShaftWidth / 2, -MarkerShaftWidth / 2, length, axis)); } // head backside vertices->push_back(getMarkerPosition(-MarkerHeadWidth / 2, -MarkerHeadWidth / 2, shaftLength, axis)); vertices->push_back(getMarkerPosition(-MarkerHeadWidth / 2, MarkerHeadWidth / 2, shaftLength, axis)); vertices->push_back(getMarkerPosition(MarkerHeadWidth / 2, MarkerHeadWidth / 2, shaftLength, axis)); vertices->push_back(getMarkerPosition(MarkerHeadWidth / 2, -MarkerHeadWidth / 2, shaftLength, axis)); // head vertices->push_back(getMarkerPosition(0, 0, shaftLength + MarkerHeadLength, axis)); geometry->setVertexArray(vertices); osg::DrawElementsUShort* primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // shaft for (int i = 0; i < 4; ++i) { int i2 = i == 3 ? 0 : i + 1; primitives->push_back(i); primitives->push_back(4 + i); primitives->push_back(i2); primitives->push_back(4 + i); primitives->push_back(4 + i2); primitives->push_back(i2); } // cap primitives->push_back(0); primitives->push_back(1); primitives->push_back(2); primitives->push_back(2); primitives->push_back(3); primitives->push_back(0); // head, backside primitives->push_back(0 + 8); primitives->push_back(1 + 8); primitives->push_back(2 + 8); primitives->push_back(2 + 8); primitives->push_back(3 + 8); primitives->push_back(0 + 8); for (int i = 0; i < 4; ++i) { primitives->push_back(12); primitives->push_back(8 + (i == 3 ? 0 : i + 1)); primitives->push_back(8 + i); } geometry->addPrimitiveSet(primitives); osg::Vec4Array* colours = new osg::Vec4Array; for (int i = 0; i < 8; ++i) colours->push_back( osg::Vec4f(axis == 0 ? 1.0f : 0.2f, axis == 1 ? 1.0f : 0.2f, axis == 2 ? 1.0f : 0.2f, mMarkerTransparency)); for (int i = 8; i < 8 + 4 + 1; ++i) colours->push_back( osg::Vec4f(axis == 0 ? 1.0f : 0.0f, axis == 1 ? 1.0f : 0.0f, axis == 2 ? 1.0f : 0.0f, mMarkerTransparency)); geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); setupCommonMarkerState(geometry); osg::ref_ptr group(new osg::Group); group->addChild(geometry); return group; } osg::ref_ptr CSVRender::Object::makeRotateMarker(int axis) { const float InnerRadius = std::max(MarkerShaftBaseLength, mBaseNode->getBound().radius()); const float OuterRadius = InnerRadius + MarkerShaftWidth; const float SegmentDistance = 100.f; const size_t SegmentCount = std::clamp(OuterRadius * 2 * osg::PI / SegmentDistance, 24, 64); const size_t VerticesPerSegment = 4; const size_t IndicesPerSegment = 24; const size_t VertexCount = SegmentCount * VerticesPerSegment; const size_t IndexCount = SegmentCount * IndicesPerSegment; const float Angle = 2 * osg::PI / SegmentCount; const unsigned short IndexPattern[IndicesPerSegment] = { 0, 4, 5, 0, 5, 1, 2, 6, 4, 2, 4, 0, 3, 7, 6, 3, 6, 2, 1, 5, 7, 1, 7, 3 }; osg::ref_ptr geometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(VertexCount); osg::ref_ptr colors = new osg::Vec4Array(1); osg::ref_ptr primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, IndexCount); // prevent some depth collision issues from overlaps osg::Vec3f offset = getMarkerPosition(0, MarkerShaftWidth / 4, 0, axis); for (size_t i = 0; i < SegmentCount; ++i) { size_t index = i * VerticesPerSegment; float innerX = InnerRadius * std::cos(i * Angle); float innerY = InnerRadius * std::sin(i * Angle); float outerX = OuterRadius * std::cos(i * Angle); float outerY = OuterRadius * std::sin(i * Angle); vertices->at(index++) = getMarkerPosition(innerX, innerY, MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(innerX, innerY, -MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(outerX, outerY, MarkerShaftWidth / 2, axis) + offset; vertices->at(index++) = getMarkerPosition(outerX, outerY, -MarkerShaftWidth / 2, axis) + offset; } colors->at(0) = osg::Vec4f(axis == 0 ? 1.0f : 0.2f, axis == 1 ? 1.0f : 0.2f, axis == 2 ? 1.0f : 0.2f, mMarkerTransparency); for (size_t i = 0; i < SegmentCount; ++i) { size_t indices[IndicesPerSegment]; for (size_t j = 0; j < IndicesPerSegment; ++j) { indices[j] = i * VerticesPerSegment + j; if (indices[j] >= VertexCount) indices[j] -= VertexCount; } size_t elementOffset = i * IndicesPerSegment; for (size_t j = 0; j < IndicesPerSegment; ++j) { primitives->setElement(elementOffset++, indices[IndexPattern[j]]); } } geometry->setVertexArray(vertices); geometry->setColorArray(colors, osg::Array::BIND_OVERALL); geometry->addPrimitiveSet(primitives); setupCommonMarkerState(geometry); osg::ref_ptr group = new osg::Group(); group->addChild(geometry); return group; } void CSVRender::Object::setupCommonMarkerState(osg::ref_ptr geometry) { osg::ref_ptr state = geometry->getOrCreateStateSet(); state->setMode(GL_LIGHTING, osg::StateAttribute::OFF); state->setMode(GL_BLEND, osg::StateAttribute::ON); state->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); } osg::Vec3f CSVRender::Object::getMarkerPosition(float x, float y, float z, int axis) { switch (axis) { case 2: return osg::Vec3f(x, y, z); case 0: return osg::Vec3f(z, x, y); case 1: return osg::Vec3f(y, z, x); default: throw std::logic_error("invalid axis for marker geometry"); } } CSVRender::Object::Object( CSMWorld::Data& data, osg::Group* parentNode, const std::string& id, bool referenceable, bool forceBaseToZero) : mData(data) , 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; mBaseNode = new osg::PositionAttitudeTransform; mBaseNode->addCullCallback(new SceneUtil::LightListCallback); mOutline = new osgFX::Scribe; mBaseNode->setUserData(new ObjectTag(this)); mRootNode->addChild(mBaseNode); parentNode->addChild(mRootNode); mRootNode->setNodeMask(Mask_Reference); ESM::RefId refId = ESM::RefId::stringRefId(id); if (referenceable) { mReferenceableId = refId; } else { mReferenceId = refId; mReferenceableId = getReference().mRefID; } adjustTransform(); update(); updateMarker(); } CSVRender::Object::~Object() { clear(); mParentNode->removeChild(mRootNode); } 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); } else mRootNode->addChild(mBaseNode); mMarkerTransparency = CSMPrefs::get()["Rendering"]["object-marker-alpha"].toDouble(); updateMarker(); } 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; } osg::ref_ptr CSVRender::Object::getBaseNode() { return mBaseNode; } bool CSVRender::Object::referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); int index = referenceables.searchId(mReferenceableId); if (index != -1 && index >= topLeft.row() && index <= bottomRight.row()) { adjustTransform(); update(); updateMarker(); return true; } return false; } bool CSVRender::Object::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::RefIdCollection& referenceables = mData.getReferenceables(); int index = referenceables.searchId(mReferenceableId); if (index != -1 && index >= start && index <= end) { // Deletion of referenceable-type objects is handled outside of Object. if (!mReferenceId.empty()) { adjustTransform(); update(); return true; } } return false; } 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); if (index != -1 && index >= topLeft.row() && index <= bottomRight.row()) { int columnIndex = references.findColumnIndex(CSMWorld::Columns::ColumnId_ReferenceableId); adjustTransform(); if (columnIndex >= topLeft.column() && columnIndex <= bottomRight.row()) { mReferenceableId = ESM::RefId::stringRefId(references.getData(index, columnIndex).toString().toUtf8().constData()); update(); updateMarker(); } return true; } return false; } void CSVRender::Object::reloadAssets() { update(); updateMarker(); } std::string CSVRender::Object::getReferenceId() const { return mReferenceId.getRefIdString(); } std::string CSVRender::Object::getReferenceableId() const { return mReferenceableId.getRefIdString(); } osg::ref_ptr CSVRender::Object::getTag() const { return static_cast(mBaseNode->getUserData()); } bool CSVRender::Object::isEdited() const { return mOverrideFlags; } void CSVRender::Object::setEdited(int flags) { bool discard = mOverrideFlags & ~flags; int added = flags & ~mOverrideFlags; mOverrideFlags = flags; if (added & Override_Position) 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) mPositionOverride.rot[i] = getReference().mPos.rot[i]; if (added & Override_Scale) mScaleOverride = getReference().mScale; if (discard) adjustTransform(); } ESM::Position CSVRender::Object::getPosition() const { ESM::Position position = getReference().mPos; if (mOverrideFlags & Override_Position) for (int i = 0; i < 3; ++i) position.pos[i] = mPositionOverride.pos[i]; if (mOverrideFlags & Override_Rotation) for (int i = 0; i < 3; ++i) position.rot[i] = mPositionOverride.rot[i]; return position; } float CSVRender::Object::getScale() const { return (mOverrideFlags & Override_Scale) ? mScaleOverride : getReference().mScale; } void CSVRender::Object::setPosition(const float position[3]) { mOverrideFlags |= Override_Position; for (int i = 0; i < 3; ++i) mPositionOverride.pos[i] = position[i]; adjustTransform(); } void CSVRender::Object::setRotation(const float rotation[3]) { mOverrideFlags |= Override_Rotation; for (int i = 0; i < 3; ++i) mPositionOverride.rot[i] = rotation[i]; adjustTransform(); } void CSVRender::Object::setScale(float scale) { mOverrideFlags |= Override_Scale; mScaleOverride = std::clamp(scale, 0.5f, 2.0f); adjustTransform(); } void CSVRender::Object::setMarkerTransparency(float value) { mMarkerTransparency = value; updateMarker(); } void CSVRender::Object::apply(CSMWorld::CommandMacro& commands) { const CSMWorld::RefCollection& collection = mData.getReferences(); QAbstractItemModel* model = mData.getTableModel(CSMWorld::UniversalId::Type_References); int recordIndex = collection.getIndex(mReferenceId); if (mOverrideFlags & Override_Position) { // 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 originalIndex = ref.getCellIndex(); 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 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) { int column = collection.findColumnIndex( static_cast(CSMWorld::Columns::ColumnId_PositionXPos + 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) { 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]))); } } if (mOverrideFlags & Override_Scale) { int column = collection.findColumnIndex(CSMWorld::Columns::ColumnId_Scale); commands.push(new CSMWorld::ModifyCommand(*model, model->index(recordIndex, column), mScaleOverride)); } mOverrideFlags = 0; } void CSVRender::Object::setSubMode(int subMode) { if (subMode != mSubMode) { mSubMode = subMode; updateMarker(); } } void CSVRender::Object::reset() { mOverrideFlags = 0; adjustTransform(); updateMarker(); } openmw-openmw-0.49.0/apps/opencs/view/render/object.hpp000066400000000000000000000132051503074453300230630ustar00rootroot00000000000000#ifndef OPENCS_VIEW_OBJECT_H #define OPENCS_VIEW_OBJECT_H #include #include #include #include #include #include #include #include "tagbase.hpp" class QModelIndex; namespace osg { class PositionAttitudeTransform; class Geometry; class Group; class Node; } namespace osgFX { class Scribe; } namespace Resource { class ResourceSystem; } namespace CSMWorld { class Data; struct CellRef; class CommandMacro; } 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 class ObjectTag : public TagBase { public: ObjectTag(Object* object); Object* mObject; QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; }; class ObjectMarkerTag : public ObjectTag { public: ObjectMarkerTag(Object* object, int axis); int mAxis; }; class Object { public: enum OverrideFlags { Override_Position = 1, Override_Rotation = 2, Override_Scale = 4 }; private: 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; /// Not implemented Object(const Object&); /// Not implemented Object& operator=(const Object&); /// 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(); /// Adjust position, orientation and scale void adjustTransform(); /// Throws an exception if *this was constructed with referenceable const CSMWorld::CellRef& getReference() const; void updateMarker(); osg::ref_ptr makeMoveOrScaleMarker(int axis); osg::ref_ptr makeRotateMarker(int axis); /// Sets up a stateset with properties common to all marker types. void setupCommonMarkerState(osg::ref_ptr geometry); osg::Vec3f getMarkerPosition(float x, float y, float z, int axis); public: Object(CSMWorld::Data& data, osg::Group* cellNode, const std::string& id, bool referenceable, bool forceBaseToZero = false); /// \param forceBaseToZero If this is a reference ignore the coordinates and place /// it at 0, 0, 0 instead. ~Object(); /// 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)); bool getSelected() const; /// Mark Object as "snap target" void setSnapTarget(bool isSnapTarget); bool getSnapTarget() const; /// Get object node with GUI graphics osg::ref_ptr getRootNode(); /// 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 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); /// Reloads the underlying asset void reloadAssets(); /// Returns an empty string if this is a refereceable-type object. std::string getReferenceId() const; std::string getReferenceableId() const; osg::ref_ptr getTag() const; /// Is there currently an editing operation running on this object? bool isEdited() const; void setEdited(int flags); ESM::Position getPosition() const; float getScale() const; /// Set override position. void setPosition(const float position[3]); /// Set override rotation void setRotation(const float rotation[3]); /// Set override scale void setScale(float scale); void setMarkerTransparency(float value); /// Apply override changes via command and end edit mode void apply(CSMWorld::CommandMacro& commands); void setSubMode(int subMode); /// Erase all overrides and restore the visual representation of the object to its /// true state. void reset(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/orbitcameramode.cpp000066400000000000000000000031431503074453300247450ustar00rootroot00000000000000#include "orbitcameramode.hpp" #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) : ModeButton(icon, tooltip, parent) , mWorldspaceWidget(worldspaceWidget) , mCenterOnSelection(nullptr) { mCenterShortcut = new CSMPrefs::Shortcut("orbit-center-selection", worldspaceWidget); mCenterShortcut->enable(false); 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, &QAction::triggered, this, &OrbitCameraMode::centerSelection); mCenterShortcut->enable(true); } void OrbitCameraMode::deactivate(CSVWidget::SceneToolbar* toolbar) { mCenterShortcut->associateAction(nullptr); mCenterShortcut->enable(false); } bool OrbitCameraMode::createContextMenu(QMenu* menu) { if (menu) { menu->addAction(mCenterOnSelection); } return true; } void OrbitCameraMode::centerSelection() { mWorldspaceWidget->centerOrbitCameraOnSelection(); } } openmw-openmw-0.49.0/apps/opencs/view/render/orbitcameramode.hpp000066400000000000000000000017631503074453300247600ustar00rootroot00000000000000#ifndef CSV_RENDER_ORBITCAMERAPICKMODE_H #define CSV_RENDER_ORBITCAMERAPICKMODE_H #include "../widget/modebutton.hpp" class QAction; class QMenu; class QObject; class QWidget; namespace CSVWidget { class SceneToolbar; } namespace CSMPrefs { class Shortcut; } namespace CSVRender { class WorldspaceWidget; class OrbitCameraMode : public CSVWidget::ModeButton { Q_OBJECT public: OrbitCameraMode(WorldspaceWidget* worldspaceWidget, const QIcon& icon, const QString& tooltip = "", QWidget* parent = nullptr); ~OrbitCameraMode() override = default; void activate(CSVWidget::SceneToolbar* toolbar) override; void deactivate(CSVWidget::SceneToolbar* toolbar) override; bool createContextMenu(QMenu* menu) override; private: WorldspaceWidget* mWorldspaceWidget; QAction* mCenterOnSelection; CSMPrefs::Shortcut* mCenterShortcut; private slots: void centerSelection(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/pagedworldspacewidget.cpp000066400000000000000000001014451503074453300261640ustar00rootroot00000000000000#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 "../../model/prefs/shortcut.hpp" #include "../../model/world/idtable.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetooltoggle2.hpp" #include "cellarrow.hpp" #include "editmode.hpp" #include "mask.hpp" #include "terrainshapemode.hpp" #include "terraintexturemode.hpp" class QWidget; namespace CSMWorld { struct Cell; } namespace CSVWidget { class SceneToolbar; } bool CSVRender::PagedWorldspaceWidget::adjustCells() { bool modified = false; const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); { // remove/update std::map::iterator iter(mCells.begin()); while (iter != mCells.end()) { if (!mSelection.has(iter->first)) { // remove delete iter->second; mCells.erase(iter++); modified = true; } else { // update const int index = cells.searchId(ESM::RefId::stringRefId(iter->first.getId(mWorldspace))); bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; if (deleted != iter->second->isDeleted()) { modified = true; auto cell = std::make_unique(mDocument, mRootNode, iter->first.getId(mWorldspace), deleted, true); delete iter->second; iter->second = cell.release(); } else if (!deleted) { // delete state has not changed -> just update // 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; modified = true; } ++iter; } } } // add for (CSMWorld::CellSelection::Iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { if (mCells.find(*iter) == mCells.end()) { addCellToScene(*iter); modified = true; } } if (modified) { 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) { 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; } if (!mSelection.has(coordinates)) mask |= i; } iter->second->setCellArrows(mask); } } return modified; } void CSVRender::PagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) { WorldspaceWidget::addVisibilitySelectorButtons(tool); tool->addButton(Button_Terrain, Mask_Terrain, "Terrain"); } void CSVRender::PagedWorldspaceWidget::addEditModeSelectorButtons(CSVWidget::SceneToolMode* 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"); 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) { if (hit.tag && hit.tag->getMask() == Mask_CellArrow) { if (CellArrowTag* cellArrowTag = dynamic_cast(hit.tag.get())) { CellArrow* arrow = cellArrowTag->getCellArrow(); CSMWorld::CellCoordinates coordinates = arrow->getCoordinates(); CellArrow::Direction direction = arrow->getDirection(); int x = 0; int y = 0; 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; } bool modified = false; if (type == InteractionType_PrimarySelect) { addCellSelection(x, y); modified = true; } else if (type == InteractionType_SecondarySelect) { moveCellSelection(x, y); modified = true; } else // Primary/SecondaryEdit { CSMWorld::CellCoordinates newCoordinates = coordinates.move(x, y); if (mCells.find(newCoordinates) == mCells.end()) { addCellToScene(newCoordinates); mSelection.add(newCoordinates); modified = true; } if (type == InteractionType_SecondaryEdit) { if (mCells.find(coordinates) != mCells.end()) { removeCellFromScene(coordinates); mSelection.remove(coordinates); modified = true; } } } if (modified) adjustCells(); return; } } WorldspaceWidget::handleInteractionPress(hit, type); } 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)) flagAsModified(); } 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)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::referenceableAdded(const QModelIndex& parent, int start, int end) { CSMWorld::IdTable& referenceables = dynamic_cast( *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables)); for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) { QModelIndex topLeft = referenceables.index(start, 0); QModelIndex bottomRight = referenceables.index(end, referenceables.columnCount()); if (iter->second->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } } 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)) flagAsModified(); } 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)) flagAsModified(); } 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)) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridModified(); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { // Pathgrid going to be deleted for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridRemoved(); flagAsModified(); } } } } void CSVRender::PagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); CSMWorld::CellCoordinates coords = CSMWorld::CellCoordinates(pathgrid.mData.mX, pathgrid.mData.mY); std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) { searchResult->second->pathgridModified(); flagAsModified(); } } } } void CSVRender::PagedWorldspaceWidget::landDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { for (int r = topLeft.row(); r <= bottomRight.row(); ++r) { const auto& id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landDataChanged(topLeft, bottomRight); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::landAboutToBeRemoved(const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { const auto& id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landAboutToBeRemoved(parent, start, end); flagAsModified(); } } } void CSVRender::PagedWorldspaceWidget::landAdded(const QModelIndex& parent, int start, int end) { for (int r = start; r <= end; ++r) { const auto& id = mDocument.getData().getLand().getId(r); auto cellIt = mCells.find(CSMWorld::CellCoordinates::fromId(id.getRefIdString()).first); if (cellIt != mCells.end()) { cellIt->second->landAdded(parent, start, end); flagAsModified(); } } } 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) { for (auto cellIt : mCells) cellIt.second->landTextureAboutToBeRemoved(parent, start, end); flagAsModified(); } 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; mView->getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d position = eye; std::ostringstream stream; stream << "player->position " << position.x() << ", " << position.y() << ", " << position.z() << ", 0"; return stream.str(); } void CSVRender::PagedWorldspaceWidget::addCellToScene(const CSMWorld::CellCoordinates& coordinates) { const CSMWorld::IdCollection& cells = mDocument.getData().getCells(); const int index = cells.searchId(ESM::RefId::stringRefId(coordinates.getId(mWorldspace))); bool deleted = index == -1 || cells.getRecord(index).mState == CSMWorld::RecordBase::State_Deleted; 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())); } void CSVRender::PagedWorldspaceWidget::removeCellFromScene(const CSMWorld::CellCoordinates& coordinates) { std::map::iterator iter = mCells.find(coordinates); if (iter != mCells.end()) { delete iter->second; mCells.erase(iter); } } void CSVRender::PagedWorldspaceWidget::addCellSelection(int x, int y) { CSMWorld::CellSelection newSelection = mSelection; newSelection.move(x, y); for (CSMWorld::CellSelection::Iterator iter(newSelection.begin()); iter != newSelection.end(); ++iter) { if (mCells.find(*iter) == mCells.end()) { addCellToScene(*iter); mSelection.add(*iter); } } } void CSVRender::PagedWorldspaceWidget::moveCellSelection(int x, int y) { CSMWorld::CellSelection newSelection = mSelection; newSelection.move(x, y); for (CSMWorld::CellSelection::Iterator iter(mSelection.begin()); iter != mSelection.end(); ++iter) { if (!newSelection.has(*iter)) removeCellFromScene(*iter); } for (CSMWorld::CellSelection::Iterator iter(newSelection.begin()); iter != newSelection.end(); ++iter) { if (!mSelection.has(*iter)) addCellToScene(*iter); } mSelection = std::move(newSelection); } void CSVRender::PagedWorldspaceWidget::addCellToSceneFromCamera(int offsetX, int offsetY) { osg::Vec3f eye, center, up; getCamera()->getViewMatrixAsLookAt(eye, center, up); int cellX = (int)std::floor(center.x() / Constants::CellSizeInUnits) + offsetX; int cellY = (int)std::floor(center.y() / Constants::CellSizeInUnits) + offsetY; CSMWorld::CellCoordinates cellCoordinates(cellX, cellY); if (!mSelection.has(cellCoordinates)) { addCellToScene(cellCoordinates); mSelection.add(cellCoordinates); adjustCells(); } } 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); connect(cells, &QAbstractItemModel::dataChanged, this, &PagedWorldspaceWidget::cellDataChanged); connect(cells, &QAbstractItemModel::rowsRemoved, this, &PagedWorldspaceWidget::cellRemoved); connect(cells, &QAbstractItemModel::rowsInserted, this, &PagedWorldspaceWidget::cellAdded); connect(&document.getData(), &CSMWorld::Data::assetTablesChanged, this, &PagedWorldspaceWidget::assetTablesChanged); QAbstractItemModel* lands = document.getData().getTableModel(CSMWorld::UniversalId::Type_Lands); 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); 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, qOverload<>(&CSMPrefs::Shortcut::activated), this, &PagedWorldspaceWidget::loadCameraCell); CSMPrefs::Shortcut* loadCameraEastCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-eastcell", this); connect(loadCameraEastCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &PagedWorldspaceWidget::loadEastCell); CSMPrefs::Shortcut* loadCameraNorthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-northcell", this); connect(loadCameraNorthCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &PagedWorldspaceWidget::loadNorthCell); CSMPrefs::Shortcut* loadCameraWestCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-westcell", this); connect(loadCameraWestCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &PagedWorldspaceWidget::loadWestCell); CSMPrefs::Shortcut* loadCameraSouthCellShortcut = new CSMPrefs::Shortcut("scene-load-cam-southcell", this); connect(loadCameraSouthCellShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &PagedWorldspaceWidget::loadSouthCell); } CSVRender::PagedWorldspaceWidget::~PagedWorldspaceWidget() { for (std::map::iterator iter(mCells.begin()); iter != mCells.end(); ++iter) { delete iter->second; } } void CSVRender::PagedWorldspaceWidget::useViewHint(const std::string& hint) { if (!hint.empty()) { CSMWorld::CellSelection selection; 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()); if (stream >> ignore) { char ignore1; // : or ; char ignore2; // # // Current coordinate int x, y; // Loop through all the coordinates to add them to selection while (stream >> ignore1 >> ignore2 >> x >> y) selection.add(CSMWorld::CellCoordinates(x, y)); // Mark that camera needs setup mCamPositionSet = false; } } else if (hint[0] == 'r') { // syntax r:ref#number (e.g. r:ref#100) char ignore; 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) { } // 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()); 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)); // Mark that camera needs setup mCamPositionSet = false; } } } setCellSelection(selection); } } void CSVRender::PagedWorldspaceWidget::setCellSelection(const CSMWorld::CellSelection& selection) { mSelection = selection; if (adjustCells()) flagAsModified(); emit cellSelectionChanged(mSelection); } const CSMWorld::CellSelection& CSVRender::PagedWorldspaceWidget::getCellSelection() const { return mSelection; } std::pair CSVRender::PagedWorldspaceWidget::getCoordinatesFromId(const std::string& record) const { 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& universalIdData, DropType type) { if (WorldspaceWidget::handleDrop(universalIdData, type)) return true; if (type != Type_CellsExterior) return false; bool selectionChanged = false; for (const auto& id : universalIdData) { std::pair coordinates(getCoordinatesFromId(id.getId())); if (mSelection.add(CSMWorld::CellCoordinates(coordinates.first, coordinates.second))) { selectionChanged = true; } } if (selectionChanged) { if (adjustCells()) flagAsModified(); emit cellSelectionChanged(mSelection); } return true; } CSVRender::WorldspaceWidget::dropRequirments CSVRender::PagedWorldspaceWidget::getDropRequirements( CSVRender::WorldspaceWidget::DropType type) const { dropRequirments requirements = WorldspaceWidget::getDropRequirements(type); if (requirements != ignored) return requirements; switch (type) { case Type_CellsExterior: return canHandle; case Type_CellsInterior: return needUnpaged; default: return ignored; } } unsigned int CSVRender::PagedWorldspaceWidget::getVisibilityMask() const { return WorldspaceWidget::getVisibilityMask() | mControlElements->getSelectionMask(); } void CSVRender::PagedWorldspaceWidget::clearSelection(int elementMask) { 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) { 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) { 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) { 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) { for (auto& cell : mCells) { cell.second->selectInsideCube(pointA, pointB, dragMode); } } void CSVRender::PagedWorldspaceWidget::selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) { for (auto& cell : mCells) { cell.second->selectWithinDistance(point, distance, dragMode); } } 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))); 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))); std::map::const_iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second; else return nullptr; } CSVRender::Cell* CSVRender::PagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const { std::map::const_iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second; else return nullptr; } 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) { std::map::iterator searchResult = mCells.find(coords); if (searchResult != mCells.end()) return searchResult->second->getAlteredHeight(inCellX, inCellY); return nullptr; } void CSVRender::PagedWorldspaceWidget::resetAllAlteredHeights() { for (const auto& cell : mCells) cell.second->resetAlteredHeights(); } 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; for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) { std::vector> cellResult = iter->second->getSelection(elementMask); result.insert(result.end(), cellResult.begin(), cellResult.end()); } return result; } 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; for (std::map::const_iterator iter = mCells.begin(); iter != mCells.end(); ++iter) { std::vector> cellResult = iter->second->getEdited(elementMask); result.insert(result.end(), cellResult.begin(), cellResult.end()); } return result; } 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); } void CSVRender::PagedWorldspaceWidget::reset(unsigned int 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) { 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->setSelectionMask(0xffffffff); connect(mControlElements, &CSVWidget::SceneToolToggle2::selectionChanged, this, &PagedWorldspaceWidget::elementSelectionChanged); return mControlElements; } 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) { if (adjustCells()) flagAsModified(); } 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()) flagAsModified(); } void CSVRender::PagedWorldspaceWidget::assetTablesChanged() { std::map::iterator iter = mCells.begin(); for (; iter != mCells.end(); ++iter) { iter->second->reloadAssets(); } } void CSVRender::PagedWorldspaceWidget::loadCameraCell() { addCellToSceneFromCamera(0, 0); } void CSVRender::PagedWorldspaceWidget::loadEastCell() { addCellToSceneFromCamera(1, 0); } void CSVRender::PagedWorldspaceWidget::loadNorthCell() { addCellToSceneFromCamera(0, 1); } void CSVRender::PagedWorldspaceWidget::loadWestCell() { addCellToSceneFromCamera(-1, 0); } void CSVRender::PagedWorldspaceWidget::loadSouthCell() { addCellToSceneFromCamera(0, -1); } openmw-openmw-0.49.0/apps/opencs/view/render/pagedworldspacewidget.hpp000066400000000000000000000164571503074453300262010ustar00rootroot00000000000000#ifndef OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_PAGEDWORLDSPACEWIDGET_H #include #include #include #include #include #include "../../model/world/cellselection.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 SceneToolToggle2; class SceneToolMode; class SceneToolBar; } namespace CSVRender { 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; private: std::pair getCoordinatesFromId(const std::string& record) const; /// 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 referenceableAboutToBeRemoved(const QModelIndex& parent, 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 referenceAboutToBeRemoved(const QModelIndex& parent, 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 pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void pathgridAdded(const QModelIndex& parent, int start, int end) 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 /// /// \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 moveCellSelection(int x, int y); void addCellToSceneFromCamera(int offsetX, int offsetY); 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. virtual ~PagedWorldspaceWidget(); /// 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); const CSMWorld::CellSelection& getCellSelection() const; /// \return Drop handled? bool handleDrop(const std::vector& data, DropType type) 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); 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 select operation void invertSelection(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; void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; std::string getCellId(const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) const override; Cell* getCell(const CSMWorld::CellCoordinates& coords) const override; void setCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY, float height); float* getCellAlteredHeight(const CSMWorld::CellCoordinates& coords, int inCellX, int inCellY); void resetAllAlteredHeights(); osg::ref_ptr getSnapTarget(unsigned int elementMask) const override; std::vector> getSelection(unsigned int elementMask) const override; void selectGroup(const std::vector& group) const override; void unhideAll() const override; std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; /// Erase all overrides and restore the visual representation to its true state. void reset(unsigned int elementMask) override; protected: void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) override; void addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool) override; void handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type) override; signals: void cellSelectionChanged(const CSMWorld::CellSelection& selection); private slots: virtual void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); virtual void cellRemoved(const QModelIndex& parent, int start, int end); virtual void cellAdded(const QModelIndex& index, 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 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 assetTablesChanged(); void loadCameraCell(); void loadEastCell(); void loadNorthCell(); void loadWestCell(); void loadSouthCell(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/pathgrid.cpp000066400000000000000000000562061503074453300234220ustar00rootroot00000000000000#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 "../../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(); } }; PathgridTag::PathgridTag(Pathgrid* pathgrid) : TagBase(Mask_Pathgrid) , mPathgrid(pathgrid) { } Pathgrid* PathgridTag::getPathgrid() const { return mPathgrid; } QString PathgridTag::getToolTip(bool /*hideBasics*/, const WorldspaceHitResult& hit) const { QString text("Pathgrid: "); text += mPathgrid->getId().c_str(); text += " ("; text += QString::number(SceneUtil::getPathgridNode(hit.index0)); text += ")"; return text; } Pathgrid::Pathgrid(CSMWorld::Data& data, osg::Group* parent, const std::string& pathgridId, const CSMWorld::CellCoordinates& coordinates) : mData(data) , mPathgridCollection(mData.getPathgrids()) , mId(ESM::RefId::stringRefId(pathgridId)) , mCoords(coordinates) , mInterior(false) , mDragOrigin(0) , mChangeGeometry(true) , mRemoveGeometry(false) , mUseOffset(true) , mParent(parent) , mPathgridGeometry(nullptr) , mDragGeometry(nullptr) , mTag(new PathgridTag(this)) { const float CoordScalar = ESM::Land::REAL_SIZE; 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); mPathgridGroup = new osg::Group(); mBaseNode->addChild(mPathgridGroup); recreateGeometry(); int index = mData.getCells().searchId(mId); if (index != -1) { const CSMWorld::Cell& cell = mData.getCells().getRecord(index).get(); mInterior = cell.mData.mFlags & ESM::Cell::Interior; } } Pathgrid::~Pathgrid() { mParent->removeChild(mBaseNode); } const CSMWorld::CellCoordinates& Pathgrid::getCoordinates() const { return mCoords; } const std::string& Pathgrid::getId() const { return mId.getRefIdString(); } bool Pathgrid::isSelected() const { return !mSelected.empty(); } const Pathgrid::NodeList& Pathgrid::getSelected() const { return mSelected; } void Pathgrid::selectAll() { mSelected.clear(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (unsigned short i = 0; i < static_cast(source->mPoints.size()); ++i) mSelected.push_back(i); createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::toggleSelected(unsigned short node) { NodeList::iterator searchResult = std::find(mSelected.begin(), mSelected.end(), node); if (searchResult != mSelected.end()) { mSelected.erase(searchResult); } else { mSelected.push_back(node); } createSelectedGeometry(); } void Pathgrid::invertSelected() { NodeList temp = NodeList(mSelected); mSelected.clear(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (unsigned short i = 0; i < static_cast(source->mPoints.size()); ++i) { if (std::find(temp.begin(), temp.end(), i) == temp.end()) mSelected.push_back(i); } createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::clearSelected() { mSelected.clear(); removeSelectedGeometry(); } void Pathgrid::moveSelected(const osg::Vec3d& offset) { mUseOffset = true; mMoveOffset += offset; recreateGeometry(); } void Pathgrid::setDragOrigin(unsigned short node) { mDragOrigin = node; } void Pathgrid::setDragEndpoint(unsigned short node) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { const CSMWorld::Pathgrid::Point& pointA = source->mPoints[mDragOrigin]; const CSMWorld::Pathgrid::Point& pointB = source->mPoints[node]; osg::Vec3f start = osg::Vec3f(pointA.mX, pointA.mY, pointA.mZ + SceneUtil::DiamondHalfHeight); osg::Vec3f end = osg::Vec3f(pointB.mX, pointB.mY, pointB.mZ + SceneUtil::DiamondHalfHeight); createDragGeometry(start, end, true); } } void Pathgrid::setDragEndpoint(const osg::Vec3d& pos) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { const CSMWorld::Pathgrid::Point& point = source->mPoints[mDragOrigin]; osg::Vec3f start = osg::Vec3f(point.mX, point.mY, point.mZ + SceneUtil::DiamondHalfHeight); osg::Vec3f end = pos - mBaseNode->getPosition(); createDragGeometry(start, end, false); } } void Pathgrid::resetIndicators() { mUseOffset = false; mMoveOffset.set(0, 0, 0); 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)); const std::string& idString = mId.getRefIdString(); const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { osg::Vec3d localCoords = worldPos - mBaseNode->getPosition(); int posX = clampToCell(static_cast(localCoords.x())); int posY = clampToCell(static_cast(localCoords.y())); int posZ = clampToCell(static_cast(localCoords.z())); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); 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, 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)); } else { int index = mPathgridCollection.searchId(mId); if (index == -1) { // Does not exist 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, 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, 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, idString, row, parentColumn)); } } } } void Pathgrid::applyPosition(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { osg::Vec3d localCoords = mMoveOffset; int offsetX = static_cast(localCoords.x()); int offsetY = static_cast(localCoords.y()); int offsetZ = static_cast(localCoords.z()); QAbstractItemModel* model = mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); int posXColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosX); int posYColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosY); int posZColumn = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridPosZ); QModelIndex parent = model->index(recordIndex, parentColumn); for (const auto& selected : mSelected) { 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, posYColumn, parent), clampToCell(point.mY + offsetY))); commands.push(new CSMWorld::ModifyCommand( *model, model->index(row, posZColumn, parent), clampToCell(point.mZ + offsetZ))); } } } void Pathgrid::applyEdge(CSMWorld::CommandMacro& commands, unsigned short node1, unsigned short node2) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { addEdge(commands, *source, node1, node2); } } void Pathgrid::applyEdges(CSMWorld::CommandMacro& commands, unsigned short node) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { for (const auto& selected : mSelected) { addEdge(commands, *source, node, selected); } } } void Pathgrid::applyRemoveNodes(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { 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()); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridPoints); for (std::vector::iterator row = mSelected.begin(); row != mSelected.end(); ++row) { commands.push(new CSMWorld::DeleteNestedCommand( *model, mId.getRefIdString(), static_cast(*row), parentColumn)); } // Fix/remove edges std::set> edgeRowsToRemove; parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); int edge1Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge1); QModelIndex parent = model->index(recordIndex, parentColumn); for (size_t edge = 0; edge < source->mEdges.size(); ++edge) { int adjustment0 = 0; int adjustment1 = 0; // Determine necessary adjustment for (const auto point : mSelected) { if (source->mEdges[edge].mV0 == point || source->mEdges[edge].mV1 == point) { edgeRowsToRemove.insert(static_cast(edge)); adjustment0 = 0; // No need to adjust, its getting removed adjustment1 = 0; break; } if (source->mEdges[edge].mV0 > point) --adjustment0; 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)); } if (adjustment1 != 0) { int adjustedEdge = source->mEdges[edge].mV1 + adjustment1; commands.push( new CSMWorld::ModifyCommand(*model, model->index(edge, edge1Column, parent), adjustedEdge)); } } for (const auto row : edgeRowsToRemove) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), row, parentColumn)); } } clearSelected(); } void Pathgrid::applyRemoveEdges(CSMWorld::CommandMacro& commands) { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { // Want to remove from end of row first std::set> rowsToRemove; for (size_t i = 0; i <= mSelected.size(); ++i) { for (size_t j = i + 1; j < mSelected.size(); ++j) { int row = edgeExists(*source, mSelected[i], mSelected[j]); if (row != -1) { rowsToRemove.insert(row); } row = edgeExists(*source, mSelected[j], mSelected[i]); if (row != -1) { rowsToRemove.insert(row); } } } CSMWorld::IdTree* model = &dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Pathgrids)); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); std::set>::iterator row; for (row = rowsToRemove.begin(); row != rowsToRemove.end(); ++row) { commands.push(new CSMWorld::DeleteNestedCommand(*model, mId.getRefIdString(), *row, parentColumn)); } } } osg::ref_ptr Pathgrid::getTag() const { return mTag; } void Pathgrid::recreateGeometry() { mChangeGeometry = true; } void Pathgrid::removeGeometry() { mRemoveGeometry = true; } void Pathgrid::update() { if (mRemoveGeometry) { removePathgridGeometry(); removeSelectedGeometry(); } else if (mChangeGeometry) { createGeometry(); } mChangeGeometry = false; mRemoveGeometry = false; } void Pathgrid::createGeometry() { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { CSMWorld::Pathgrid temp; if (mUseOffset) { temp = *source; for (NodeList::iterator it = mSelected.begin(); it != mSelected.end(); ++it) { temp.mPoints[*it].mX += mMoveOffset.x(); temp.mPoints[*it].mY += mMoveOffset.y(); temp.mPoints[*it].mZ += mMoveOffset.z(); } source = &temp; } removePathgridGeometry(); mPathgridGeometry = SceneUtil::createPathgridGeometry(*source); mPathgridGroup->addChild(mPathgridGeometry); createSelectedGeometry(*source); } else { removePathgridGeometry(); removeSelectedGeometry(); } } void Pathgrid::createSelectedGeometry() { const CSMWorld::Pathgrid* source = getPathgridSource(); if (source) { createSelectedGeometry(*source); } else { removeSelectedGeometry(); } } void Pathgrid::createSelectedGeometry(const CSMWorld::Pathgrid& source) { removeSelectedGeometry(); mSelectedGeometry = SceneUtil::createPathgridSelectedWireframe(source, mSelected); mPathgridGroup->addChild(mSelectedGeometry); } void Pathgrid::removePathgridGeometry() { if (mPathgridGeometry) { mPathgridGroup->removeChild(mPathgridGeometry); mPathgridGeometry = nullptr; } } void Pathgrid::removeSelectedGeometry() { if (mSelectedGeometry) { mPathgridGroup->removeChild(mSelectedGeometry); mSelectedGeometry = nullptr; } } void Pathgrid::createDragGeometry(const osg::Vec3f& start, const osg::Vec3f& end, bool valid) { if (mDragGeometry) mPathgridGroup->removeChild(mDragGeometry); mDragGeometry = new osg::Geometry(); osg::ref_ptr vertices = new osg::Vec3Array(2); osg::ref_ptr colors = new osg::Vec4Array(1); osg::ref_ptr indices = new osg::DrawElementsUShort(osg::PrimitiveSet::LINES, 2); (*vertices)[0] = start; (*vertices)[1] = end; if (valid) { (*colors)[0] = osg::Vec4f(0.91f, 0.66f, 1.f, 1.f); } else { (*colors)[0] = osg::Vec4f(1.f, 0.f, 0.f, 1.f); } indices->setElement(0, 0); indices->setElement(1, 1); mDragGeometry->setVertexArray(vertices); mDragGeometry->setColorArray(colors, osg::Array::BIND_OVERALL); mDragGeometry->addPrimitiveSet(indices); mDragGeometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); mPathgridGroup->addChild(mDragGeometry); } const CSMWorld::Pathgrid* Pathgrid::getPathgridSource() { int index = mPathgridCollection.searchId(mId); if (index != -1 && !mPathgridCollection.getRecord(index).isDeleted()) { return &mPathgridCollection.getRecord(index).get(); } return nullptr; } int Pathgrid::edgeExists(const CSMWorld::Pathgrid& source, unsigned short node1, unsigned short node2) { for (size_t i = 0; i < source.mEdges.size(); ++i) { if (source.mEdges[i].mV0 == node1 && source.mEdges[i].mV1 == node2) return static_cast(i); } return -1; } 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)); int recordIndex = mPathgridCollection.getIndex(mId); int parentColumn = mPathgridCollection.findColumnIndex(CSMWorld::Columns::ColumnId_PathgridEdges); int edge0Column = mPathgridCollection.searchNestedColumnIndex(parentColumn, CSMWorld::Columns::ColumnId_PathgridEdge0); 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.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; } if (edgeExists(source, node2, node1) == -1) { 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)); } } int Pathgrid::clampToCell(int v) { const int CellExtent = ESM::Land::REAL_SIZE; if (mInterior) return v; else if (v > CellExtent) return CellExtent; else if (v < 0) return 0; else return v; } } openmw-openmw-0.49.0/apps/opencs/view/render/pathgrid.hpp000066400000000000000000000075061503074453300234260ustar00rootroot00000000000000#ifndef CSV_RENDER_PATHGRID_H #define CSV_RENDER_PATHGRID_H #include #include #include #include #include #include #include "../../model/world/cellcoordinates.hpp" #include "../../model/world/subcellcollection.hpp" #include "tagbase.hpp" #include namespace osg { class Vec3f; class Geometry; class Group; class PositionAttitudeTransform; } namespace CSMWorld { class CommandMacro; class Data; struct Pathgrid; } namespace CSVRender { class Pathgrid; struct WorldspaceHitResult; class PathgridTag : public TagBase { public: PathgridTag(Pathgrid* pathgrid); Pathgrid* getPathgrid() const; QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const override; 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); ~Pathgrid(); const CSMWorld::CellCoordinates& getCoordinates() const; const std::string& getId() const; bool isSelected() const; const NodeList& getSelected() const; void selectAll(); void toggleSelected(unsigned short node); // Adds to end of vector void invertSelected(); void clearSelected(); void moveSelected(const osg::Vec3d& offset); void setDragOrigin(unsigned short node); void setDragEndpoint(unsigned short node); void setDragEndpoint(const osg::Vec3d& pos); void resetIndicators(); 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); osg::ref_ptr getTag() const; void recreateGeometry(); void removeGeometry(); void update(); private: CSMWorld::Data& mData; CSMWorld::SubCellCollection& mPathgridCollection; ESM::RefId mId; CSMWorld::CellCoordinates mCoords; bool mInterior; NodeList mSelected; osg::Vec3d mMoveOffset; unsigned short mDragOrigin; bool mChangeGeometry; bool mRemoveGeometry; bool mUseOffset; 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; 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); 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 clampToCell(int v); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/pathgridmode.cpp000066400000000000000000000234731503074453300242670ustar00rootroot00000000000000#include "pathgridmode.hpp" #include #include #include #include "../../model/prefs/state.hpp" #include "../../model/world/commandmacro.hpp" #include "../widget/scenetoolbar.hpp" #include "cell.hpp" #include "mask.hpp" #include "pathgrid.hpp" #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, Misc::ScalableIcon::load(":scenetoolbar/editing-pathgrid"), Mask_Pathgrid | Mask_Terrain | Mask_Reference, getTooltip(), parent) , mDragMode(DragMode_None) , mFromNode(0) , mSelectionMode(nullptr) { } QString PathgridMode::getTooltip() { return QString( "Pathgrid editing" "
  • Press {scene-edit-primary} to add a node to the cursor location
  • " "
  • Press {scene-edit-secondary} to connect the selected nodes to the node beneath the cursor
  • " "
  • Press {scene-edit-primary} and drag to move selected nodes
  • " "
  • Press {scene-edit-secondary} and drag to connect one node to another
  • " "

Note: Only a single cell's pathgrid may be edited at a time"); } void PathgridMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mSelectionMode) { mSelectionMode = new PathgridSelectionMode(toolbar, getWorldspaceWidget()); } EditMode::activate(toolbar); toolbar->addTool(mSelectionMode); } void PathgridMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if (mSelectionMode) { toolbar->removeTool(mSelectionMode); delete mSelectionMode; mSelectionMode = nullptr; } } 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())) { primarySelectPressed(hitResult); } else if (Cell* cell = getWorldspaceWidget().getCell(hitResult.worldPos)) { if (cell->getPathgrid()) { // Add node QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Add node"; CSMWorld::CommandMacro macro(undoStack, description); cell->getPathgrid()->applyPoint(macro, hitResult.worldPos); } } } void PathgridMode::secondaryEditPressed(const WorldspaceHitResult& hit) { if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->isSelected()) { unsigned short node = SceneUtil::getPathgridNode(hit.index0); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Connect node to selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyEdges(macro, node); } } } } void PathgridMode::primarySelectPressed(const WorldspaceHitResult& hit) { getWorldspaceWidget().clearSelection(Mask_Pathgrid); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { mLastId = tag->getPathgrid()->getId(); unsigned short node = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->toggleSelected(node); } } } void PathgridMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->getId() != mLastId) { getWorldspaceWidget().clearSelection(Mask_Pathgrid); mLastId = tag->getPathgrid()->getId(); } unsigned short node = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->toggleSelected(node); return; } } getWorldspaceWidget().clearSelection(Mask_Pathgrid); } bool PathgridMode::primaryEditStartDrag(const QPoint& pos) { std::vector> selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); if (CSMPrefs::get()["3D Scene Input"]["context-select"].isTrue()) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (dynamic_cast(hit.tag.get())) { primarySelectPressed(hit); selection = getWorldspaceWidget().getSelection(Mask_Pathgrid); } } if (!selection.empty()) { mDragMode = DragMode_Move; return true; } return false; } bool PathgridMode::secondaryEditStartDrag(const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { mDragMode = DragMode_Edge; mEdgeId = tag->getPathgrid()->getId(); mFromNode = SceneUtil::getPathgridNode(hit.index0); tag->getPathgrid()->setDragOrigin(mFromNode); return true; } } return false; } void PathgridMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == DragMode_Move) { 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())) { osg::Vec3d eye, center, up, offset; getWorldspaceWidget().getCamera()->getViewMatrix().getLookAt(eye, center, up); offset = (up * diffY * speedFactor) + (((center - eye) ^ up) * diffX * speedFactor); tag->getPathgrid()->moveSelected(offset); } } } else if (mDragMode == DragMode_Edge) { 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) { unsigned short node = SceneUtil::getPathgridNode(hit.index0); cell->getPathgrid()->setDragEndpoint(node); } else { cell->getPathgrid()->setDragEndpoint(hit.worldPos); } } } } void PathgridMode::dragCompleted(const QPoint& pos) { if (mDragMode == DragMode_Move) { 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())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Move pathgrid node(s)"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyPosition(macro); } } } else if (mDragMode == DragMode_Edge) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); if (hit.tag) { if (PathgridTag* tag = dynamic_cast(hit.tag.get())) { if (tag->getPathgrid()->getId() == mEdgeId) { unsigned short toNode = SceneUtil::getPathgridNode(hit.index0); QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Add edge between nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyEdge(macro, mFromNode, toNode); } } } mEdgeId.clear(); mFromNode = 0; } mDragMode = DragMode_None; getWorldspaceWidget().reset(Mask_Pathgrid); } void PathgridMode::dragAborted() { getWorldspaceWidget().reset(Mask_Pathgrid); } } openmw-openmw-0.49.0/apps/opencs/view/render/pathgridmode.hpp000066400000000000000000000032611503074453300242650ustar00rootroot00000000000000#ifndef CSV_RENDER_PATHGRIDMODE_H #define CSV_RENDER_PATHGRIDMODE_H #include #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); void activate(CSVWidget::SceneToolbar* toolbar) override; void deactivate(CSVWidget::SceneToolbar* toolbar) override; void primaryOpenPressed(const WorldspaceHitResult& hit) override; void primaryEditPressed(const WorldspaceHitResult& hit) override; void secondaryEditPressed(const WorldspaceHitResult& hit) override; void primarySelectPressed(const WorldspaceHitResult& hit) override; void secondarySelectPressed(const WorldspaceHitResult& hit) override; bool primaryEditStartDrag(const QPoint& pos) override; bool secondaryEditStartDrag(const QPoint& pos) override; void drag(const QPoint& pos, int diffX, int diffY, double speedFactor) 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; private: enum DragMode { DragMode_None, DragMode_Move, DragMode_Edge }; DragMode mDragMode; std::string mLastId, mEdgeId; unsigned short mFromNode; PathgridSelectionMode* mSelectionMode; QString getTooltip(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/pathgridselectionmode.cpp000066400000000000000000000053111503074453300261640ustar00rootroot00000000000000#include "pathgridselectionmode.hpp" #include #include #include #include #include "../../model/world/commandmacro.hpp" #include #include #include #include #include "pathgrid.hpp" #include "worldspacewidget.hpp" namespace CSVWidget { class SceneToolbar; } namespace CSVRender { PathgridSelectionMode::PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget) : SelectionMode(parent, worldspaceWidget, Mask_Pathgrid) { mRemoveSelectedNodes = new QAction("Remove selected nodes", this); mRemoveSelectedEdges = new QAction("Remove edges between selected nodes", this); connect(mRemoveSelectedNodes, &QAction::triggered, this, &PathgridSelectionMode::removeSelectedNodes); connect(mRemoveSelectedEdges, &QAction::triggered, this, &PathgridSelectionMode::removeSelectedEdges); } bool PathgridSelectionMode::createContextMenu(QMenu* menu) { if (menu) { SelectionMode::createContextMenu(menu); menu->addAction(mRemoveSelectedNodes); menu->addAction(mRemoveSelectedEdges); } return true; } void PathgridSelectionMode::removeSelectedNodes() { 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())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Remove selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyRemoveNodes(macro); } } } void PathgridSelectionMode::removeSelectedEdges() { 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())) { QUndoStack& undoStack = getWorldspaceWidget().getDocument().getUndoStack(); QString description = "Remove edges between selected nodes"; CSMWorld::CommandMacro macro(undoStack, description); tag->getPathgrid()->applyRemoveEdges(macro); } } } } openmw-openmw-0.49.0/apps/opencs/view/render/pathgridselectionmode.hpp000066400000000000000000000017261503074453300261770ustar00rootroot00000000000000#ifndef CSV_RENDER_PATHGRID_SELECTION_MODE_H #define CSV_RENDER_PATHGRID_SELECTION_MODE_H #include "selectionmode.hpp" namespace CSVRender { class WorldspaceWidget; } namespace CSVWidget { class SceneToolbar; } namespace CSVRender { class PathgridSelectionMode : public SelectionMode { Q_OBJECT public: PathgridSelectionMode(CSVWidget::SceneToolbar* parent, WorldspaceWidget& worldspaceWidget); 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; private: QAction* mRemoveSelectedNodes; QAction* mRemoveSelectedEdges; private slots: void removeSelectedNodes(); void removeSelectedEdges(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/previewwidget.cpp000066400000000000000000000111131503074453300244710ustar00rootroot00000000000000#include "previewwidget.hpp" #include #include #include #include #include #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" 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); connect(referenceables, &QAbstractItemModel::dataChanged, this, &PreviewWidget::referenceableDataChanged); connect( referenceables, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PreviewWidget::referenceableAboutToBeRemoved); connect(&mData, &CSMWorld::Data::assetTablesChanged, this, &PreviewWidget::assetTablesChanged); setExterior(false); if (!referenceable) { QAbstractItemModel* references = mData.getTableModel(CSMWorld::UniversalId::Type_References); connect(references, &QAbstractItemModel::dataChanged, this, &PreviewWidget::referenceDataChanged); connect(references, &QAbstractItemModel::rowsAboutToBeRemoved, this, &PreviewWidget::referenceAboutToBeRemoved); } } void CSVRender::PreviewWidget::referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mObject.referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) { CSMWorld::IdTable& referenceables = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Referenceables)); QModelIndex index = referenceables.getModelIndex( mObject.getReferenceableId(), referenceables.findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); if (referenceables.data(index).toInt() == CSMWorld::RecordBase::State_Deleted) emit closeRequest(); } } void CSVRender::PreviewWidget::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mObject.referenceableAboutToBeRemoved(parent, start, end)) flagAsModified(); if (mObject.getReferenceableId().empty()) return; CSMWorld::IdTable& referenceables = dynamic_cast(*mData.getTableModel(CSMWorld::UniversalId::Type_Referenceables)); QModelIndex index = referenceables.getModelIndex(mObject.getReferenceableId(), 0); if (index.row() >= start && index.row() <= end) { if (mObject.getReferenceId().empty()) { // this is a preview for a referenceble emit closeRequest(); } } } void CSVRender::PreviewWidget::referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mObject.referenceDataChanged(topLeft, bottomRight)) flagAsModified(); if (mObject.getReferenceId().empty()) return; 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)); if (references.data(index).toInt() == CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); return; } } int columnIndex = references.findColumnIndex(CSMWorld::Columns::ColumnId_ReferenceableId); 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()); } 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)); QModelIndex index = references.getModelIndex(mObject.getReferenceId(), 0); if (index.row() >= start && index.row() <= end) emit closeRequest(); } void CSVRender::PreviewWidget::assetTablesChanged() { mObject.reloadAssets(); } openmw-openmw-0.49.0/apps/opencs/view/render/previewwidget.hpp000066400000000000000000000020601503074453300244770ustar00rootroot00000000000000#ifndef OPENCS_VIEW_PREVIEWWIDGET_H #define OPENCS_VIEW_PREVIEWWIDGET_H #include "scenewidget.hpp" #include #include "object.hpp" class QModelIndex; class QObject; class QWidget; namespace CSMWorld { class Data; } namespace CSVRender { class PreviewWidget : public SceneWidget { Q_OBJECT CSMWorld::Data& mData; CSVRender::Object mObject; public: PreviewWidget(CSMWorld::Data& data, const std::string& id, bool referenceable, QWidget* parent = nullptr); signals: void closeRequest(); void referenceableIdChanged(const std::string& id); private slots: void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end); void referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end); void assetTablesChanged(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/scenewidget.cpp000066400000000000000000000510631503074453300241150ustar00rootroot00000000000000#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 "../widget/scenetoolmode.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "cameracontroller.hpp" #include "lighting.hpp" #include "mask.hpp" namespace CSVRender { RenderWidget::RenderWidget(QWidget* parent, Qt::WindowFlags f) : QWidget(parent, f) , mRootNode(nullptr) { mView = new osgViewer::View; updateCameraParameters(width() / static_cast(height())); 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()); int frameRateLimit = CSMPrefs::get()["Rendering"]["framerate-limit"].toInt(); mRenderer->setRunMaxFrameRate(frameRateLimit); mRenderer->setUseConfigureAffinity(false); QLayout* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(mWidget); setLayout(layout); mView->getCamera()->setGraphicsContext(window); osg::ref_ptr lightMgr = new SceneUtil::LightManager; lightMgr->setStartLight(1); lightMgr->setLightingMask(Mask_Lighting); mRootNode = std::move(lightMgr); mView->getCamera()->setViewport(new osg::Viewport(0, 0, width(), height())); 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); mRenderer->addView(mView); mRenderer->setDone(false); } RenderWidget::~RenderWidget() { try { mRenderer->removeView(mView); } catch (const std::exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } delete mWidget; } 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()); window->getEventQueue()->keyPress(osgGA::GUIEventAdapter::KEY_S); window->getEventQueue()->keyRelease(osgGA::GUIEventAdapter::KEY_S); } // --------------------------------------------------- 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; 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); mView->getCamera()->setClearMask(0); mView->getCamera()->addChild(mGradientCamera.get()); } else { mView->getCamera()->setClearColor(osg::Vec4(bgColour.redF(), bgColour.greenF(), bgColour.blueF(), 1.0f)); } // we handle lighting manually mView->setLightingMode(osgViewer::View::NO_LIGHT); setLighting(&mLightingDay); mResourceSystem->getSceneManager()->setParticleSystemMask(Mask_ParticleSystem); // Recieve mouse move event even if mouse button is not pressed setMouseTracking(true); setFocusPolicy(Qt::ClickFocus); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &SceneWidget::settingChanged); // 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(); } connect(mRenderer, &CompositeOsgRenderer::simulationUpdated, this, &SceneWidget::update); // Shortcuts CSMPrefs::Shortcut* focusToolbarShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); connect( focusToolbarShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneWidget::focusToolbarRequest); CSMPrefs::Shortcut* renderStatsShortcut = new CSMPrefs::Shortcut("scene-render-stats", this); connect( renderStatsShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneWidget::toggleRenderStats); } 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 SceneWidget::createGradientRectangle(QColor& bgColour, QColor& gradientColour) { osg::ref_ptr geometry = new osg::Geometry; osg::ref_ptr vertices = new osg::Vec3Array; 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)); geometry->setVertexArray(vertices); osg::ref_ptr primitives = new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, 0); // triangle 1 primitives->push_back(0); primitives->push_back(1); primitives->push_back(2); // triangle 2 primitives->push_back(2); primitives->push_back(1); primitives->push_back(3); geometry->addPrimitiveSet(primitives); 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)); geometry->setColorArray(colours, osg::Array::BIND_PER_VERTEX); geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); geometry->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); return geometry; } 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()); camera->setClearMask(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); camera->setAllowEventFocus(false); // draw subgraph before main camera view. camera->setRenderOrder(osg::Camera::PRE_RENDER); camera->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); osg::ref_ptr gradientQuad = createGradientRectangle(bgColour, gradientColour); camera->addChild(std::move(gradientQuad)); return camera; } void SceneWidget::updateGradientCamera(QColor& bgColour, QColor& gradientColour) { osg::ref_ptr gradientRect = createGradientRectangle(bgColour, gradientColour); // Replaces previous rectangle mGradientCamera->setChild(0, gradientRect.get()); } void SceneWidget::setLighting(Lighting* lighting) { if (mLighting) mLighting->deactivate(); mLighting = lighting; mLighting->activate(mRootNode, mIsExterior); osg::Vec4f ambient = mLighting->getAmbientColour(mHasDefaultAmbient ? &mDefaultAmbient : nullptr); setAmbient(ambient); flagAsModified(); } 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); } 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 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()); } } } 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, &CSVWidget::SceneToolMode::modeChanged, this, &SceneWidget::selectLightingMode); 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) { mCurrentCamControl->update(dt); } else { mCurrentCamControl->setup(mRootNode, Mask_Reference | Mask_Terrain, CameraController::WorldUp); mCamPositionSet = 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()); } 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()) { 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; 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); } } 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(); } else if (mode == "orbit") { mCurrentCamControl->setCamera(nullptr); mCurrentCamControl = mOrbitCamControl; mOrbitCamControl->setCamera(getCamera()); mOrbitCamControl->reset(); } } } openmw-openmw-0.49.0/apps/opencs/view/render/scenewidget.hpp000066400000000000000000000074741503074453300241310ustar00rootroot00000000000000#ifndef OPENCS_VIEW_SCENEWIDGET_H #define OPENCS_VIEW_SCENEWIDGET_H #include #include #include #include #include #include #include #include #include #include "lightingbright.hpp" #include "lightingday.hpp" #include "lightingnight.hpp" class QMouseEvent; class QWheelEvent; class osgQOpenGLWidget; class CompositeOsgRenderer; namespace Resource { class ResourceSystem; } namespace osg { class Group; class Camera; class Geometry; } namespace osg { class View; } namespace CSVWidget { class SceneToolMode; class SceneToolbar; } namespace CSMPrefs { class Setting; } namespace CSVRender { class CameraController; class FreeCameraController; class OrbitCameraController; class Lighting; class RenderWidget : public QWidget { Q_OBJECT public: RenderWidget(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); virtual ~RenderWidget(); /// Initiates a request to redraw the view void flagAsModified(); void setVisibilityMask(unsigned int mask); osg::Camera* getCamera(); protected: osgQOpenGLWidget* mWidget; CompositeOsgRenderer* mRenderer; osg::ref_ptr mView; osg::ref_ptr mRootNode; void updateCameraParameters(double overrideAspect = -1.0); protected slots: 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); protected: void setLighting(Lighting* lighting); ///< \attention The ownership of \a lighting is not transferred to *this. void setAmbient(const osg::Vec4f& ambient); void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; osg::ref_ptr createGradientRectangle(QColor& bgColour, QColor& gradientColour); osg::ref_ptr createGradientCamera(QColor& bgColour, QColor& gradientColour); void updateGradientCamera(QColor& bgColour, QColor& gradientColour); std::shared_ptr mResourceSystem; Lighting* mLighting; osg::ref_ptr mGradientCamera; osg::Vec4f mDefaultAmbient; bool mHasDefaultAmbient; bool mIsExterior; LightingDay mLightingDay; LightingNight mLightingNight; LightingBright mLightingBright; int mPrevMouseX, mPrevMouseY; /// Tells update that camera isn't set bool mCamPositionSet; FreeCameraController* mFreeCamControl; OrbitCameraController* mOrbitCamControl; CameraController* mCurrentCamControl; public slots: void update(double dt); protected slots: virtual void settingChanged(const CSMPrefs::Setting* setting); void selectNavigationMode(const std::string& mode); private slots: void selectLightingMode(const std::string& mode); signals: void focusToolbarRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/selectionmode.cpp000066400000000000000000000065461503074453300244540ustar00rootroot00000000000000#include "selectionmode.hpp" #include #include #include #include #include "worldspacewidget.hpp" namespace CSVWidget { class SceneToolbar; } namespace CSVRender { 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.
  • " "
  • 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
  • " "
"); 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
  • " "
  • 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
  • " "
"); 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
  • " "
  • 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
  • " "
"); mSelectAll = new QAction("Select all", this); mDeselectAll = new QAction("Clear selection", this); mInvertSelection = new QAction("Invert selection", this); connect(mSelectAll, &QAction::triggered, this, &SelectionMode::selectAll); connect(mDeselectAll, &QAction::triggered, this, &SelectionMode::clearSelection); connect(mInvertSelection, &QAction::triggered, this, &SelectionMode::invertSelection); } WorldspaceWidget& SelectionMode::getWorldspaceWidget() { return mWorldspaceWidget; } bool SelectionMode::createContextMenu(QMenu* menu) { if (menu) { menu->addAction(mSelectAll); menu->addAction(mDeselectAll); menu->addAction(mInvertSelection); } return true; } void SelectionMode::selectAll() { getWorldspaceWidget().selectAll(mInteractionMask); } void SelectionMode::clearSelection() { getWorldspaceWidget().clearSelection(mInteractionMask); } void SelectionMode::invertSelection() { getWorldspaceWidget().invertSelection(mInteractionMask); } } openmw-openmw-0.49.0/apps/opencs/view/render/selectionmode.hpp000066400000000000000000000022561503074453300244530ustar00rootroot00000000000000#ifndef CSV_RENDER_SELECTION_MODE_H #define CSV_RENDER_SELECTION_MODE_H #include "../widget/scenetoolmode.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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/tagbase.cpp000066400000000000000000000005031503074453300232130ustar00rootroot00000000000000#include "tagbase.hpp" #include CSVRender::TagBase::TagBase(Mask mask) : mMask(mask) { } CSVRender::Mask CSVRender::TagBase::getMask() const { return mMask; } QString CSVRender::TagBase::getToolTip(bool hideBasics, const WorldspaceHitResult& /*hit*/) const { return ""; } openmw-openmw-0.49.0/apps/opencs/view/render/tagbase.hpp000066400000000000000000000006621503074453300232260ustar00rootroot00000000000000#ifndef OPENCS_VIEW_TAGBASE_H #define OPENCS_VIEW_TAGBASE_H #include #include #include "mask.hpp" namespace CSVRender { struct WorldspaceHitResult; class TagBase : public osg::Referenced { Mask mMask; public: TagBase(Mask mask); Mask getMask() const; virtual QString getToolTip(bool hideBasics, const WorldspaceHitResult& hit) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/terrainselection.cpp000066400000000000000000000334671503074453300251760ustar00rootroot00000000000000#include "terrainselection.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cell.hpp" #include "worldspacewidget.hpp" 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(); } CSVRender::TerrainSelection::~TerrainSelection() { deactivate(); } std::vector> CSVRender::TerrainSelection::getTerrainSelection() const { return mSelection; } void CSVRender::TerrainSelection::onlySelect(const std::vector>& localPositions) { mSelection = localPositions; update(); } void CSVRender::TerrainSelection::addSelect(const std::vector>& localPositions) { handleSelection(localPositions, SelectionMethod::AddSelect); } void CSVRender::TerrainSelection::removeSelect(const std::vector>& localPositions) { handleSelection(localPositions, SelectionMethod::RemoveSelect); } void CSVRender::TerrainSelection::toggleSelect(const std::vector>& localPositions) { handleSelection(localPositions, SelectionMethod::ToggleSelect); } void CSVRender::TerrainSelection::clearTemporarySelection() { mTemporarySelection.clear(); } void CSVRender::TerrainSelection::activate() { if (!mParentNode->containsNode(mSelectionNode)) mParentNode->addChild(mSelectionNode); } void CSVRender::TerrainSelection::deactivate() { mParentNode->removeChild(mSelectionNode); } void CSVRender::TerrainSelection::update() { mSelectionNode->removeChild(mGeometry); mGeometry = new osg::Geometry(); const osg::ref_ptr vertices(new osg::Vec3Array); switch (mSelectionType) { 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); mSelectionNode->addChild(mGeometry); } void CSVRender::TerrainSelection::drawShapeSelection(const osg::ref_ptr vertices) { if (!mSelection.empty()) { for (std::pair& localPos : mSelection) { 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)); 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))); 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))); } } } void CSVRender::TerrainSelection::drawTextureSelection(const osg::ref_ptr vertices) { if (!mSelection.empty()) { const int landHeightsNudge = (ESM::Land::REAL_SIZE / ESM::Land::LAND_SIZE) / (ESM::Land::LAND_SIZE - 1); // Does this work with all land size configurations? const int textureSizeToLandSizeModifier = (ESM::Land::LAND_SIZE - 1) / ESM::Land::LAND_TEXTURE_SIZE; for (std::pair& localPos : mSelection) { int x(localPos.first); int y(localPos.second); // convert texture selection to global vertex coordinates at selection box corners int x1 = x * textureSizeToLandSizeModifier + landHeightsNudge; int x2 = x * textureSizeToLandSizeModifier + textureSizeToLandSizeModifier + landHeightsNudge; int y1 = y * textureSizeToLandSizeModifier - landHeightsNudge; int y2 = y * textureSizeToLandSizeModifier + textureSizeToLandSizeModifier - landHeightsNudge; // Draw edges (check all sides, draw lines between vertices, +1 height to keep lines above ground) // Check adjancent selections, draw lines only to edges of the selection const auto north = std::find(mSelection.begin(), mSelection.end(), std::make_pair(x, y + 1)); if (north == mSelection.end()) { 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 + 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++) { 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++) { 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++) { 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, SelectionMethod selectionMethod) { for (auto const& localPos : localPositions) { const auto iter = std::find(mSelection.begin(), mSelection.end(), localPos); switch (selectionMethod) { case SelectionMethod::OnlySelect: break; 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: { const auto iterTemp = std::find(mTemporarySelection.begin(), mTemporarySelection.end(), localPos); if (iterTemp == mTemporarySelection.end()) { if (iter == mSelection.end()) { mSelection.emplace_back(localPos); } else { mSelection.erase(iter); } } mTemporarySelection.emplace_back(localPos); break; } default: break; } } update(); } bool CSVRender::TerrainSelection::noCell(const std::string& cellId) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); const CSMWorld::IdCollection& cellCollection = document.getData().getCells(); 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(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(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; return false; } int CSVRender::TerrainSelection::calculateLandHeight(int x, int y) // global vertex coordinates { int cellX = std::floor(static_cast(x) / (ESM::Land::LAND_SIZE - 1)); int cellY = std::floor(static_cast(y) / (ESM::Land::LAND_SIZE - 1)); int localX = x - cellX * (ESM::Land::LAND_SIZE - 1); int localY = y - cellY * (ESM::Land::LAND_SIZE - 1); CSMWorld::CellCoordinates coords(cellX, cellY); float landHeight = 0.f; if (CSVRender::Cell* cell = dynamic_cast(mWorldspaceWidget->getCell(coords))) { landHeight = cell->getSumOfAlteredAndTrueHeight(cellX, cellY, localX, localY); } else if (isLandLoaded(CSMWorld::CellCoordinates::generateId(cellX, cellY))) { CSMDoc::Document& document = mWorldspaceWidget->getDocument(); std::string cellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); 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; } openmw-openmw-0.49.0/apps/opencs/view/render/terrainselection.hpp000066400000000000000000000046141503074453300251730ustar00rootroot00000000000000#ifndef CSV_RENDER_TERRAINSELECTION_H #define CSV_RENDER_TERRAINSELECTION_H #include #include #include #include #include namespace osg { class Group; class Geometry; class PositionAttitudeTransform; } namespace CSVRender { class WorldspaceWidget; enum class TerrainSelectionType { Texture, Shape }; enum class SelectionMethod { OnlySelect, AddSelect, RemoveSelect, ToggleSelect }; /// \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); void removeSelect(const std::vector>& localPositions); void toggleSelect(const std::vector>& localPositions); void clearTemporarySelection(); void activate(); void deactivate(); std::vector> getTerrainSelection() const; void update(); protected: void drawShapeSelection(const osg::ref_ptr vertices); void drawTextureSelection(const osg::ref_ptr vertices); int calculateLandHeight(int x, int y); private: void handleSelection(const std::vector>& localPositions, SelectionMethod selectionMethod); bool noCell(const std::string& cellId); bool noLand(const std::string& cellId); bool noLandLoaded(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 TerrainSelectionType mSelectionType; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/terrainshapemode.cpp000066400000000000000000002377751503074453300251660ustar00rootroot00000000000000#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 "../widget/scenetoolbar.hpp" #include "../widget/scenetoolshapebrush.hpp" #include "../../model/prefs/state.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtree.hpp" #include "brushdraw.hpp" #include "commands.hpp" #include "editmode.hpp" #include "mask.hpp" #include "pagedworldspacewidget.hpp" #include "terrainselection.hpp" #include "worldspacewidget.hpp" 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) { } void CSVRender::TerrainShapeMode::activate(CSVWidget::SceneToolbar* toolbar) { if (!mTerrainShapeSelection) { mTerrainShapeSelection = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Shape); } if (!mShapeBrushScenetool) { 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 = std::make_unique(mParentNode); EditMode::activate(toolbar); toolbar->addTool(mShapeBrushScenetool); } void CSVRender::TerrainShapeMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if (mShapeBrushScenetool) { toolbar->removeTool(mShapeBrushScenetool); } if (mTerrainShapeSelection) { mTerrainShapeSelection.reset(); } if (mBrushDraw) mBrushDraw.reset(); EditMode::deactivate(toolbar); } void CSVRender::TerrainShapeMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here { } void CSVRender::TerrainShapeMode::primaryEditPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); if (mDragMode == InteractionType_PrimaryEdit && mShapeEditTool != ShapeEditTool_Drag) { editTerrainShapeGrid(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), true); applyTerrainEditChanges(); } } clearTransientEdits(); } void CSVRender::TerrainShapeMode::primarySelectPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 0); mTerrainShapeSelection->clearTemporarySelection(); } } void CSVRender::TerrainShapeMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { selectTerrainShapes(CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos), 1); mTerrainShapeSelection->clearTemporarySelection(); } } bool CSVRender::TerrainShapeMode::primaryEditStartDrag(const QPoint& pos) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(pos, getWorldspaceWidget().getInteractionMask()); mDragMode = InteractionType_PrimaryEdit; if (hit.hit && hit.tag == nullptr) { mEditingPos = hit.worldPos; mIsEditing = true; if (mShapeEditTool == ShapeEditTool_Flatten || mShapeEditTool == ShapeEditTool_Equalize) setFlattenToolTargetHeight(hit); } return true; } bool CSVRender::TerrainShapeMode::secondaryEditStartDrag(const QPoint& pos) { return false; } bool CSVRender::TerrainShapeMode::primarySelectStartDrag(const QPoint& pos) { 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); return true; } bool CSVRender::TerrainShapeMode::secondarySelectStartDrag(const QPoint& pos) { 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); return true; } void CSVRender::TerrainShapeMode::drag(const QPoint& pos, int diffX, int diffY, double speedFactor) { if (mDragMode == InteractionType_PrimaryEdit) { 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 (mDragMode == InteractionType_PrimarySelect) { 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); } } void CSVRender::TerrainShapeMode::dragCompleted(const QPoint& pos) { if (mDragMode == InteractionType_PrimaryEdit) { applyTerrainEditChanges(); clearTransientEdits(); } if (mDragMode == InteractionType_PrimarySelect || mDragMode == InteractionType_SecondarySelect) { mTerrainShapeSelection->clearTemporarySelection(); } } void CSVRender::TerrainShapeMode::dragAborted() { clearTransientEdits(); mDragMode = InteractionType_None; } void CSVRender::TerrainShapeMode::dragWheel(int diff, double speedFactor) {} void CSVRender::TerrainShapeMode::sortAndLimitAlteredCells() { bool passing = false; int passes = 0; std::sort(mAlteredCells.begin(), mAlteredCells.end()); mAlteredCells.erase(std::unique(mAlteredCells.begin(), mAlteredCells.end()), mAlteredCells.end()); while (!passing) // Multiple passes are needed when steepness problems arise for both x and y axis simultaneously { passing = true; 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) { 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."; clearTransientEdits(); return; } } } void CSVRender::TerrainShapeMode::clearTransientEdits() { mTotalDiffY = 0; mIsEditing = false; mAlteredCells.clear(); if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) paged->resetAllAlteredHeights(); mTerrainShapeSelection->update(); } 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)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); QUndoStack& undoStack = document.getUndoStack(); sortAndLimitAlteredCells(); 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(&getWorldspaceWidget())); 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(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget()); // Generate land height record for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { 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); else landShapeNew[j * ESM::Land::LAND_SIZE + i] = 0; } } pushEditToCommand(landShapeNew, document, landTable, cellId); } 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(); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); // Generate land normals record for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { 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]; else { 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]; } 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); if (isLandLoaded(shiftedCellId)) v2.z() = landDownShapePointer[ESM::Land::LAND_SIZE + i] - landShapePointer[j * ESM::Land::LAND_SIZE + i]; } osg::Vec3f normal = v1 ^ v2; const float hyp = normal.length() / 127.0f; normal /= hyp; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 0] = normal.x(); landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = normal.y(); landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = normal.z(); } } 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(&getWorldspaceWidget())); undoStack.endMacro(); clearTransientEdits(); } float CSVRender::TerrainShapeMode::calculateBumpShape(float distance, int radius, float height) { float distancePerRadius = distance / radius; 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 (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { if (mShapeEditTool == ShapeEditTool_Drag) paged->resetAllAlteredHeights(); } if (mBrushShape == CSVWidget::BrushShape_Point) { std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); 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_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); } 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 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_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); } 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 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); int distanceX = abs(i - vertexCoords.first); int distanceY = abs(j - vertexCoords.second); 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); // 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_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); } 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()) { for (auto const& value : mCustomBrushShape) { 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_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); } 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); } } } mTerrainShapeSelection->update(); } void CSVRender::TerrainShapeMode::setFlattenToolTargetHeight(const WorldspaceHitResult& hit) { std::pair vertexCoords = CSMWorld::CellCoordinates::toVertexCoords(hit.worldPos); std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); int inCellX = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.first); int inCellY = CSMWorld::CellCoordinates::vertexGlobalToInCellCoords(vertexCoords.second); 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); 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) { std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); if (!(allowLandShapeEditing(cellId, useTool) && (useTool || (isLandLoaded(cellId))))) return; CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget()); if (!paged) return; std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); std::string cellUpLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() - 1); std::string cellUpRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() - 1); std::string cellDownLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY() + 1); std::string cellDownRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY() + 1); if (useTool) { mAlteredCells.emplace_back(cellCoords); if (mShapeEditTool == ShapeEditTool_Drag) { // Get distance from modified land, alter land change based on zoom osg::Vec3d eye, center, up; paged->getCamera()->getViewMatrixAsLookAt(eye, center, up); 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 (inCellX != 0 && inCellY != 0 && inCellX != ESM::Land::LAND_SIZE - 1 && inCellY != ESM::Land::LAND_SIZE - 1) 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)) { 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; } else if ((inCellX == 0 && inCellY == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellDownLeftId))) { 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); paged->setCellAlteredHeight(cornerCellCoords, ESM::Land::LAND_SIZE - 1, 0, alteredHeight); } 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)) { 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, 0, ESM::Land::LAND_SIZE - 1, alteredHeight); } else return; } 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)) { 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, 0, 0, alteredHeight); } else return; } // Change values of edging cells if ((inCellX == 0) && (useTool || isLandLoaded(cellLeftId))) { 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); 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)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, -1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) mAlteredCells.emplace_back(edgeCellCoords); paged->setCellAlteredHeight(cellCoords, inCellX, inCellY, alteredHeight); paged->setCellAlteredHeight(edgeCellCoords, inCellX, ESM::Land::LAND_SIZE - 1, alteredHeight); } } if ((inCellX == ESM::Land::LAND_SIZE - 1) && (useTool || isLandLoaded(cellRightId))) { 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); 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)) { CSMWorld::CellCoordinates edgeCellCoords = cellCoords.move(0, 1); if (useTool && std::find(mAlteredCells.begin(), mAlteredCells.end(), edgeCellCoords) == mAlteredCells.end()) 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) { if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { 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()); 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. // this = this Cell // left = x - 1, up = y - 1, right = x + 1, down = y + 1 // Altered = transient edit (in current edited) float thisHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX]; float* thisAlteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY); float thisAlteredHeight = thisAlteredHeightPtr != nullptr ? *thisAlteredHeightPtr : 0.f; float leftHeight = thisHeight; float leftAlteredHeight = thisAlteredHeight; float rightHeight = thisHeight; float rightAlteredHeight = thisAlteredHeight; float downHeight = thisHeight; float downAlteredHeight = thisAlteredHeight; float upHeight = thisHeight; float upAlteredHeight = thisAlteredHeight; if (allowLandShapeEditing(cellId)) { // 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()); if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landLeftShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); leftHeight = landLeftShapePointer[inCellY * ESM::Land::LAND_SIZE + (ESM::Land::LAND_SIZE - 2)]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(-1, 0), ESM::Land::LAND_SIZE - 2, inCellY); leftAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellY == 0) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landUpShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); upHeight = landUpShapePointer[(ESM::Land::LAND_SIZE - 2) * ESM::Land::LAND_SIZE + inCellX]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(0, -1), inCellX, ESM::Land::LAND_SIZE - 2); upAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellX > 0) { leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); leftAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellY > 0) { upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); upAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellX == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landRightShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); rightHeight = landRightShapePointer[inCellY * ESM::Land::LAND_SIZE + 1]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); rightAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellY == ESM::Land::LAND_SIZE - 1) { cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landDownShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); downHeight = landDownShapePointer[1 * ESM::Land::LAND_SIZE + inCellX]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); downAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } } if (inCellX < ESM::Land::LAND_SIZE - 1) { rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); rightAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } if (inCellY < ESM::Land::LAND_SIZE - 1) { downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; float* alteredHeightPtr = paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1); downAlteredHeight = alteredHeightPtr != nullptr ? *alteredHeightPtr : 0.f; } 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) { 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); float thisHeight = 0.0f; float thisAlteredHeight = 0.0f; std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); 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); } 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)); 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()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() - 1); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY() + 1); *thisHeight = 0.0f; // real + altered height *thisAlteredHeight = 0.0f; // only altered height *leftHeight = 0.0f; *leftAlteredHeight = 0.0f; *upHeight = 0.0f; *upAlteredHeight = 0.0f; *rightHeight = 0.0f; *rightAlteredHeight = 0.0f; *downHeight = 0.0f; *downAlteredHeight = 0.0f; if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { if (!noCell(cellId) && !noLand(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); 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(). *leftHeight = *thisHeight; *upHeight = *thisHeight; *rightHeight = *thisHeight; *downHeight = *thisHeight; // If at edge, get values from neighboring cell if (inCellX == 0) { if (isLandLoaded(cellLeftId)) { 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); *leftHeight += *leftAlteredHeight; } } } if (inCellY == 0) { if (isLandLoaded(cellUpId)) { 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)) { *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)) { 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)) { *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(1, 0), 1, inCellY); *rightHeight += *rightAlteredHeight; } } } if (inCellY == ESM::Land::LAND_SIZE - 1) { if (isLandLoaded(cellDownId)) { 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)) { *downAlteredHeight = *paged->getCellAlteredHeight(cellCoords.move(0, 1), inCellX, 1); *downHeight += *downAlteredHeight; } } } // If not at edge, get values from the same cell if (inCellX != 0) { *leftHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX - 1]; if (paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY)) *leftAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX - 1, inCellY); *leftHeight += *leftAlteredHeight; } if (inCellY != 0) { *upHeight = landShapePointer[(inCellY - 1) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1)) *upAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX, inCellY - 1); *upHeight += *upAlteredHeight; } if (inCellX != ESM::Land::LAND_SIZE - 1) { *rightHeight = landShapePointer[inCellY * ESM::Land::LAND_SIZE + inCellX + 1]; if (paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY)) *rightAlteredHeight = *paged->getCellAlteredHeight(cellCoords, inCellX + 1, inCellY); *rightHeight += *rightAlteredHeight; } if (inCellY != ESM::Land::LAND_SIZE - 1) { *downHeight = landShapePointer[(inCellY + 1) * ESM::Land::LAND_SIZE + inCellX]; if (paged->getCellAlteredHeight(cellCoords, inCellX, inCellY + 1)) *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) { if (limitedAlteredHeightXAxis) { if (limitedAlteredHeightYAxis) { if (std::abs(*limitedAlteredHeightXAxis) >= std::abs(*limitedAlteredHeightYAxis)) { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); *steepnessIsWithinLimits = false; } else { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); *steepnessIsWithinLimits = false; } } else { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightXAxis, false); *steepnessIsWithinLimits = false; } } else if (limitedAlteredHeightYAxis) { alterHeight(cellCoords, inCellX, inCellY, *limitedAlteredHeightYAxis, false); *steepnessIsWithinLimits = false; } } 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)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); int limitHeightChange = 1016.0f; // Limited by save format bool steepnessIsWithinLimits = true; if (isLandLoaded(cellId)) { const CSMWorld::LandHeightsColumn::DataType landShapePointer = landTable.data(landTable.getModelIndex(cellId, landshapeColumn)) .value(); float thisHeight = 0.0f; float thisAlteredHeight = 0.0f; float leftHeight = 0.0f; float leftAlteredHeight = 0.0f; float upHeight = 0.0f; float upAlteredHeight = 0.0f; float rightHeight = 0.0f; float rightAlteredHeight = 0.0f; float downHeight = 0.0f; float downAlteredHeight = 0.0f; if (!reverseMode) { for (int inCellY = 0; inCellY < ESM::Land::LAND_SIZE; ++inCellY) { 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); // Check for height limits on x-axis if (leftHeight - thisHeight > limitHeightChange) limitedAlteredHeightXAxis = std::make_unique( leftHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (leftHeight - thisHeight < -limitHeightChange) limitedAlteredHeightXAxis = std::make_unique( leftHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (upHeight - thisHeight > limitHeightChange) limitedAlteredHeightYAxis = std::make_unique(upHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (upHeight - thisHeight < -limitHeightChange) 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); } } } if (reverseMode) { for (int inCellY = ESM::Land::LAND_SIZE - 1; inCellY >= 0; --inCellY) { 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); // Check for height limits on x-axis if (rightHeight - thisHeight > limitHeightChange) limitedAlteredHeightXAxis = std::make_unique( rightHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (rightHeight - thisHeight < -limitHeightChange) limitedAlteredHeightXAxis = std::make_unique( rightHeight + limitHeightChange - (thisHeight - thisAlteredHeight)); // Check for height limits on y-axis if (downHeight - thisHeight > limitHeightChange) limitedAlteredHeightYAxis = std::make_unique( downHeight - limitHeightChange - (thisHeight - thisAlteredHeight)); else if (downHeight - thisHeight < -limitHeightChange) 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); } } } } return steepnessIsWithinLimits; } bool CSVRender::TerrainShapeMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { std::pair vertexCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::vertexGlobalToCellId(vertexCoords); return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first) && isLandLoaded(cellId); } return false; } void CSVRender::TerrainShapeMode::handleSelection( int globalSelectionX, int globalSelectionY, std::vector>* selections) { if (isInCellSelection(globalSelectionX, globalSelectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); else { int moduloX = globalSelectionX % (ESM::Land::LAND_SIZE - 1); int moduloY = globalSelectionY % (ESM::Land::LAND_SIZE - 1); bool xIsAtCellBorder = moduloX == 0; bool yIsAtCellBorder = moduloY == 0; if (!xIsAtCellBorder && !yIsAtCellBorder) return; int selectionX = globalSelectionX; int selectionY = globalSelectionY; /* 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 (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. SE corner: (-1, +1) offset to reach current cell. */ if (isInCellSelection(globalSelectionX - 1, globalSelectionY - 1) || isInCellSelection(globalSelectionX + 1, globalSelectionY - 1) || isInCellSelection(globalSelectionX - 1, globalSelectionY + 1)) { selections->emplace_back(globalSelectionX, globalSelectionY); } } else if (xIsAtCellBorder) { selectionX--; } else if (yIsAtCellBorder) { selectionY--; } if (isInCellSelection(selectionX, selectionY)) selections->emplace_back(globalSelectionX, globalSelectionY); } } void CSVRender::TerrainShapeMode::selectTerrainShapes(const std::pair& vertexCoords, unsigned char selectMode) { int r = mBrushSize / 2; std::vector> selections; if (mBrushShape == CSVWidget::BrushShape_Point) { handleSelection(vertexCoords.first, vertexCoords.second, &selections); } if (mBrushShape == CSVWidget::BrushShape_Square) { for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { for (int j = vertexCoords.second - r; j <= vertexCoords.second + r; ++j) { handleSelection(i, j, &selections); } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { for (int i = vertexCoords.first - r; i <= vertexCoords.first + r; ++i) { 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)); // Using floating-point radius here to prevent selecting too few vertices. if (distance <= mBrushSize / 2.0f) handleSelection(i, j, &selections); } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { if (!mCustomBrushShape.empty()) { for (auto const& value : mCustomBrushShape) { std::pair localVertexCoords( vertexCoords.first + value.first, vertexCoords.second + value.second); handleSelection(localVertexCoords.first, localVertexCoords.second, &selections); } } } 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") mTerrainShapeSelection->onlySelect(selections); else if (selectAction == "Add to selection") mTerrainShapeSelection->addSelect(selections); else if (selectAction == "Remove from selection") mTerrainShapeSelection->removeSelect(selections); else if (selectAction == "Invert selection") mTerrainShapeSelection->toggleSelect(selections); } 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))); QUndoStack& undoStack = document.getUndoStack(); 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) { QVariant changedLand; changedLand.setValue(newLandGrid); QModelIndex index( landTable.getModelIndex(cellId, landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex))); QUndoStack& undoStack = document.getUndoStack(); 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(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(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(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; 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)); int landshapeColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandHeightsIndex); int landnormalsColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandNormalsIndex); float defaultHeight = 0.f; int averageDivider = 0; CSMWorld::CellCoordinates cellLeftCoords = cellCoords.move(-1, 0); CSMWorld::CellCoordinates cellRightCoords = cellCoords.move(1, 0); CSMWorld::CellCoordinates cellUpCoords = cellCoords.move(0, -1); CSMWorld::CellCoordinates cellDownCoords = cellCoords.move(0, 1); std::string cellId = CSMWorld::CellCoordinates::generateId(cellCoords.getX(), cellCoords.getY()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellLeftCoords.getX(), cellLeftCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellRightCoords.getX(), cellRightCoords.getY()); std::string cellUpId = CSMWorld::CellCoordinates::generateId(cellUpCoords.getX(), cellUpCoords.getY()); std::string cellDownId = CSMWorld::CellCoordinates::generateId(cellDownCoords.getX(), cellDownCoords.getY()); float leftCellSampleHeight = 0.0f; float rightCellSampleHeight = 0.0f; 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(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); CSMWorld::LandNormalsColumn::DataType landNormalsNew(landNormalsPointer); if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { if (isLandLoaded(cellLeftId)) { 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); } if (isLandLoaded(cellRightId)) { 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)) 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(); ++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); } if (isLandLoaded(cellDownId)) { 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)) downCellSampleHeight += *paged->getCellAlteredHeight(cellDownCoords, ESM::Land::LAND_SIZE / 2, 0); } } if (averageDivider > 0) defaultHeight = (leftCellSampleHeight + rightCellSampleHeight + upCellSampleHeight + downCellSampleHeight) / averageDivider; for (int i = 0; i < ESM::Land::LAND_SIZE; ++i) { 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; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 1] = 0; landNormalsNew[(j * ESM::Land::LAND_SIZE + i) * 3 + 2] = 127; } } QVariant changedShape; 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)); } 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)); if (noCell(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode == "Discard") return false; if (mode == "Create cell and land, then edit" && useTool) { 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); } } } else if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); 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(); if (mode == "Discard") return false; if (mode == "Show cell and edit" && useTool) { selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); paged->setCellSelection(selection); } } } if (noLand(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode == "Discard") return false; if (mode == "Create cell and land, then edit" && useTool) { document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId)); createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); sortAndLimitAlteredCells(); } } else if (noLandLoaded(cellId)) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); if (mode == "Discard") return false; if (mode == "Create cell and land, then edit" && useTool) { createNewLandData(CSMWorld::CellCoordinates::fromId(cellId).first); fixEdges(CSMWorld::CellCoordinates::fromId(cellId).first); sortAndLimitAlteredCells(); } } if (useTool && (noCell(cellId) || noLand(cellId) || noLandLoaded(cellId))) { Log(Debug::Warning) << "Land creation failed at cell id: " << cellId; return false; } return true; } void CSVRender::TerrainShapeMode::fixEdges(CSMWorld::CellCoordinates cellCoords) { 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()); std::string cellLeftId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() - 1, cellCoords.getY()); std::string cellRightId = CSMWorld::CellCoordinates::generateId(cellCoords.getX() + 1, cellCoords.getY()); 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(); CSMWorld::LandHeightsColumn::DataType landShapeNew(landShapePointer); 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]) 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]) 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))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); } void CSVRender::TerrainShapeMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainShapeMode::mouseMoveEvent(QMouseEvent* event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw && !(mShapeEditTool == ShapeEditTool_Drag && mIsEditing)) mBrushDraw->hide(); } std::shared_ptr CSVRender::TerrainShapeMode::getTerrainSelection() { return mTerrainShapeSelection; } void CSVRender::TerrainShapeMode::setBrushSize(int brushSize) { mBrushSize = brushSize; } void CSVRender::TerrainShapeMode::setBrushShape(CSVWidget::BrushShape brushShape) { mBrushShape = brushShape; // Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainShapeSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainShapeSelection->getTerrainSelection(); int selectionCenterX = 0; int selectionCenterY = 0; int selectionAmount = 0; for (auto const& value : terrainSelection) { selectionCenterX = selectionCenterX + value.first; selectionCenterY = selectionCenterY + value.second; ++selectionAmount; } if (selectionAmount != 0) { selectionCenterX /= selectionAmount; selectionCenterY /= selectionAmount; } mCustomBrushShape.clear(); std::pair differentialPos{}; for (auto const& value : terrainSelection) { differentialPos.first = value.first - selectionCenterX; differentialPos.second = value.second - selectionCenterY; mCustomBrushShape.push_back(differentialPos); } } } void CSVRender::TerrainShapeMode::setShapeEditTool(int shapeEditTool) { mShapeEditTool = shapeEditTool; } void CSVRender::TerrainShapeMode::setShapeEditToolStrength(int shapeEditToolStrength) { mShapeEditToolStrength = shapeEditToolStrength; } CSVRender::PagedWorldspaceWidget& CSVRender::TerrainShapeMode::getPagedWorldspaceWidget() { return dynamic_cast(getWorldspaceWidget()); } openmw-openmw-0.49.0/apps/opencs/view/render/terrainshapemode.hpp000066400000000000000000000205371503074453300251550ustar00rootroot00000000000000#ifndef CSV_RENDER_TERRAINSHAPEMODE_H #define CSV_RENDER_TERRAINSHAPEMODE_H #include "editmode.hpp" #include #include #include #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/columnimp.hpp" #include "../widget/brushshapes.hpp" #endif #include "brushdraw.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: 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 }; /// Editmode for terrain shape grid TerrainShapeMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); void primaryOpenPressed(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; void secondarySelectPressed(const WorldspaceHitResult&) override; void activate(CSVWidget::SceneToolbar*) override; void deactivate(CSVWidget::SceneToolbar*) 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; /// 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; /// 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; std::shared_ptr getTerrainSelection(); private: /// Remove duplicates and sort mAlteredCells, then limitAlteredHeights forward and reverse void sortAndLimitAlteredCells(); /// Reset everything in the current edit void clearTransientEdits(); /// Move pending alteredHeights changes to omwgame/omwaddon -data void applyTerrainEditChanges(); /// 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); /// 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 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 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); /// 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 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); /// 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 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 noLand(const std::string& cellId); bool noLandLoaded(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 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); 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(); public slots: void setBrushSize(int brushSize); void setBrushShape(CSVWidget::BrushShape brushShape); void setShapeEditTool(int shapeEditTool); void setShapeEditToolStrength(int shapeEditToolStrength); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/terrainstorage.cpp000066400000000000000000000152601503074453300246440ustar00rootroot00000000000000#include "terrainstorage.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace CSVRender { TerrainStorage::TerrainStorage(const CSMWorld::Data& data) : ESMTerrain::Storage(data.getResourceSystem()->getVFS()) , mData(data) { resetHeights(); } 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 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); } const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { 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 } void TerrainStorage::resetHeights() { std::fill(std::begin(mAlteredHeight), std::end(mAlteredHeight), 0); } float TerrainStorage::getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY) { float height = 0.f; 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]; } 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, std::span heightData) const { return heightData[col * ESM::Land::LAND_SIZE + row] + mAlteredHeight[static_cast(col * ESM::Land::LAND_SIZE + row)]; } int TerrainStorage::getLeftHeight(int col, int row, std::span heightData) const { 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, std::span heightData) const { 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, std::span heightData) const { 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, std::span heightData) const { 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, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getLeftHeight(col, row, heightData)); } 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, std::span heightData) const { return abs(getThisHeight(col, row, heightData) - getUpHeight(col, row, heightData)); } 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, std::span heightData) const { return getHeightDifferenceToLeft(col, row, heightData) >= heightWarningLimit || getHeightDifferenceToUp(col, row, heightData) >= heightWarningLimit; } bool TerrainStorage::rightOrDownIsOverTheLimit( int col, int row, int heightWarningLimit, std::span heightData) const { return getHeightDifferenceToRight(col, row, heightData) >= heightWarningLimit || getHeightDifferenceToDown(col, row, heightData) >= heightWarningLimit; } 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->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; color.b() = 0; } } float TerrainStorage::getAlteredHeight(int col, int row) const { return mAlteredHeight[static_cast(col * ESM::Land::LAND_SIZE + row)]; } } openmw-openmw-0.49.0/apps/opencs/view/render/terrainstorage.hpp000066400000000000000000000050531503074453300246500ustar00rootroot00000000000000#ifndef OPENCS_RENDER_TERRAINSTORAGE_H #define OPENCS_RENDER_TERRAINSTORAGE_H #include #include #include #include #include namespace CSMWorld { class Data; } namespace osg { class Vec4ub; } namespace CSVRender { /** * @brief A bridge between the terrain component and OpenCS's terrain data storage. */ class TerrainStorage : public ESMTerrain::Storage { public: TerrainStorage(const CSMWorld::Data& data); void setAlteredHeight(int inCellX, int inCellY, float heightMap); void resetHeights(); bool useAlteration() const override { return true; } float getSumOfAlteredAndTrueHeight(int cellX, int cellY, int inCellX, int inCellY); float* getAlteredHeight(int inCellX, int inCellY); private: const CSMWorld::Data& mData; std::array mAlteredHeight; 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, ESM::RefId worldspace) override; 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::LandData* heightData, osg::Vec4ub& color) const override; float getAlteredHeight(int col, int row) const override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/terraintexturemode.cpp000066400000000000000000000750201503074453300255450ustar00rootroot00000000000000#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 "../widget/scenetoolbar.hpp" #include "../widget/scenetooltexturebrush.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/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "brushdraw.hpp" #include "editmode.hpp" #include "mask.hpp" #include "pagedworldspacewidget.hpp" #include "worldspacewidget.hpp" 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) { 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 = std::make_shared(mParentNode, &getWorldspaceWidget(), TerrainSelectionType::Texture); } if (!mBrushDraw) mBrushDraw = std::make_unique(mParentNode, true); EditMode::activate(toolbar); toolbar->addTool(mTextureBrushScenetool); } void CSVRender::TerrainTextureMode::deactivate(CSVWidget::SceneToolbar* toolbar) { if (mTextureBrushScenetool) { toolbar->removeTool(mTextureBrushScenetool); delete mTextureBrushScenetool; mTextureBrushScenetool = nullptr; } if (mTerrainTextureSelection) { mTerrainTextureSelection.reset(); } if (mBrushDraw) mBrushDraw.reset(); EditMode::deactivate(toolbar); } void CSVRender::TerrainTextureMode::primaryOpenPressed(const WorldspaceHitResult& hit) // Apply changes here { } 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)); mCellId = getWorldspaceWidget().getCellId(hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); 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.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } undoStack.endMacro(); } } void CSVRender::TerrainTextureMode::primarySelectPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 0); mTerrainTextureSelection->clearTemporarySelection(); } } void CSVRender::TerrainTextureMode::secondarySelectPressed(const WorldspaceHitResult& hit) { if (hit.hit && hit.tag == nullptr) { selectTerrainTextures(CSMWorld::CellCoordinates::toTextureCoords(hit.worldPos), 1); mTerrainTextureSelection->clearTemporarySelection(); } } bool CSVRender::TerrainTextureMode::primaryEditStartDrag(const QPoint& pos) { 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)); mCellId = getWorldspaceWidget().getCellId(hit.worldPos); QUndoStack& undoStack = document.getUndoStack(); mDragMode = InteractionType_PrimaryEdit; 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"); mIsEditing = true; if (allowLandTextureEditing(mCellId)) { undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, mCellId)); editTerrainTextureGrid(hit); } } return true; } bool CSVRender::TerrainTextureMode::secondaryEditStartDrag(const QPoint& pos) { return false; } bool CSVRender::TerrainTextureMode::primarySelectStartDrag(const QPoint& pos) { 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); return true; } bool CSVRender::TerrainTextureMode::secondarySelectStartDrag(const QPoint& pos) { 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); return true; } 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); CSMDoc::Document& document = getWorldspaceWidget().getDocument(); CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted() && hit.hit && hit.tag == nullptr) { editTerrainTextureGrid(hit); } } if (mDragMode == InteractionType_PrimarySelect) { 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); } } void CSVRender::TerrainTextureMode::dragCompleted(const QPoint& pos) { if (mDragMode == InteractionType_PrimaryEdit && mIsEditing) { CSMDoc::Document& document = getWorldspaceWidget().getDocument(); QUndoStack& undoStack = document.getUndoStack(); CSMWorld::IdCollection& landtexturesCollection = document.getData().getLandTextures(); const int index = landtexturesCollection.searchId(mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { undoStack.endMacro(); mIsEditing = false; } } mTerrainTextureSelection->clearTemporarySelection(); } void CSVRender::TerrainTextureMode::dragAborted() {} 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 = 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)); mCellId = getWorldspaceWidget().getCellId(hit.worldPos); if (allowLandTextureEditing(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)); if (xHitInCell < 0) { xHitInCell = xHitInCell + landTextureSize; cellX = cellX - 1; } if (yHitInCell > 15) { yHitInCell = yHitInCell - landTextureSize; cellY = cellY + 1; } mCellId = CSMWorld::CellCoordinates::generateId(cellX, cellY); if (allowLandTextureEditing(mCellId)) { } std::string iteratedCellId; int textureColumn = landTable.findColumnIndex(CSMWorld::Columns::ColumnId_LandTexturesIndex); // 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 newTerrain = landTable.data(landTable.getModelIndex(mCellId, textureColumn)) .value(); if (allowLandTextureEditing(mCellId)) { 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 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++; for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); if (allowLandTextureEditing(iteratedCellId)) { 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++) { if (i_cell == cellX && j_cell == cellY && abs(i - xHitInCell) < r && abs(j - yHitInCell) < r) { 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; } } } pushEditToCommand(newTerrain, document, landTable, iteratedCellId); } } } } 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 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++; for (int i_cell = upperLeftCellX; i_cell <= lowerrightCellX; i_cell++) { for (int j_cell = upperLeftCellY; j_cell <= lowerrightCellY; j_cell++) { iteratedCellId = CSMWorld::CellCoordinates::generateId(i_cell, j_cell); if (allowLandTextureEditing(iteratedCellId)) { 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++) { 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))); float rf = static_cast(mBrushSize) / 2; 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))); float rf = static_cast(mBrushSize) / 2; if (distance < rf) newTerrain[j * landTextureSize + i] = brushInt; } } } pushEditToCommand(newTerrain, document, landTable, iteratedCellId); } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { CSMWorld::LandTexturesColumn::DataType newTerrain = landTable.data(landTable.getModelIndex(mCellId, textureColumn)) .value(); if (allowLandTextureEditing(mCellId) && !mCustomBrushShape.empty()) { for (auto const& value : mCustomBrushShape) { 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; } else { 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); if (allowLandTextureEditing(cellId)) { CSMWorld::LandTexturesColumn::DataType newTerrainOtherCell = landTable.data(landTable.getModelIndex(cellId, textureColumn)) .value(); newTerrainOtherCell[yInOtherCell * landTextureSize + xInOtherCell] = brushInt; pushEditToCommand(newTerrainOtherCell, document, landTable, std::move(cellId)); } } } pushEditToCommand(newTerrain, document, landTable, mCellId); } } } bool CSVRender::TerrainTextureMode::isInCellSelection(int globalSelectionX, int globalSelectionY) { if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { std::pair textureCoords = std::make_pair(globalSelectionX, globalSelectionY); std::string cellId = CSMWorld::CellCoordinates::textureGlobalToCellId(textureCoords); return paged->getCellSelection().has(CSMWorld::CellCoordinates::fromId(cellId).first); } return false; } 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 (mBrushShape == CSVWidget::BrushShape_Square) { for (int i = -r; i <= r; i++) { for (int j = -r; j <= r; j++) { int x = i + texCoords.first; int y = j + texCoords.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } if (mBrushShape == CSVWidget::BrushShape_Circle) { for (int i = -r; i <= r; i++) { for (int j = -r; j <= r; j++) { osg::Vec2f coords(i, j); float rf = static_cast(mBrushSize) / 2; if (std::round(coords.length()) < rf) { int x = i + texCoords.first; int y = j + texCoords.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } } if (mBrushShape == CSVWidget::BrushShape_Custom) { if (!mCustomBrushShape.empty()) { for (auto const& value : mCustomBrushShape) { int x = texCoords.first + value.first; int y = texCoords.second + value.second; if (isInCellSelection(x, y)) { selections.emplace_back(x, y); } } } } 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) { 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))); QUndoStack& undoStack = document.getUndoStack(); undoStack.push(new CSMWorld::ModifyCommand(landTable, index, changedLand)); undoStack.push(new CSMWorld::TouchLandCommand(landTable, ltexTable, cellId)); } 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)); 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") return false; if (mode == "Create cell and land, then edit") { 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); } } } else if (CSVRender::PagedWorldspaceWidget* paged = dynamic_cast(&getWorldspaceWidget())) { CSMWorld::CellSelection selection = paged->getCellSelection(); 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(); if (mode == "Discard") return false; if (mode == "Show cell and edit") { selection.add(CSMWorld::CellCoordinates::fromId(cellId).first); paged->setCellSelection(selection); } } } if (noLand) { std::string mode = CSMPrefs::get()["3D Scene Editing"]["outside-landedit"].toString(); // target cell does not exist if (mode == "Discard") return false; if (mode == "Create cell and land, then edit") { document.getUndoStack().push(new CSMWorld::CreateCommand(landTable, cellId)); } } return true; } void CSVRender::TerrainTextureMode::dragMoveEvent(QDragMoveEvent* event) {} void CSVRender::TerrainTextureMode::mouseMoveEvent(QMouseEvent* event) { WorldspaceHitResult hit = getWorldspaceWidget().mousePick(event->pos(), getInteractionMask()); if (hit.hit && mBrushDraw) mBrushDraw->update(hit.worldPos, mBrushSize, mBrushShape); if (!hit.hit && mBrushDraw) mBrushDraw->hide(); } std::shared_ptr CSVRender::TerrainTextureMode::getTerrainSelection() { return mTerrainTextureSelection; } void CSVRender::TerrainTextureMode::setBrushSize(int brushSize) { mBrushSize = brushSize; } void CSVRender::TerrainTextureMode::setBrushShape(CSVWidget::BrushShape brushShape) { mBrushShape = brushShape; // Set custom brush shape if (mBrushShape == CSVWidget::BrushShape_Custom && !mTerrainTextureSelection->getTerrainSelection().empty()) { auto terrainSelection = mTerrainTextureSelection->getTerrainSelection(); int selectionCenterX = 0; int selectionCenterY = 0; int selectionAmount = 0; for (auto const& value : terrainSelection) { selectionCenterX += value.first; selectionCenterY += value.second; ++selectionAmount; } if (selectionAmount != 0) { selectionCenterX /= selectionAmount; selectionCenterY /= selectionAmount; } mCustomBrushShape.clear(); for (auto const& value : terrainSelection) mCustomBrushShape.emplace_back(value.first - selectionCenterX, value.second - selectionCenterY); } } void CSVRender::TerrainTextureMode::setBrushTexture(ESM::RefId brushTexture) { mBrushTexture = brushTexture; } openmw-openmw-0.49.0/apps/opencs/view/render/terraintexturemode.hpp000066400000000000000000000104721503074453300255520ustar00rootroot00000000000000#ifndef CSV_RENDER_TERRAINTEXTUREMODE_H #define CSV_RENDER_TERRAINTEXTUREMODE_H #include "editmode.hpp" #include #include #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/columnimp.hpp" #include "../widget/brushshapes.hpp" #include "brushdraw.hpp" #endif #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 }; /// \brief Editmode for terrain texture grid TerrainTextureMode(WorldspaceWidget*, osg::Group* parentNode, QWidget* parent = nullptr); void primaryOpenPressed(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; void secondarySelectPressed(const WorldspaceHitResult&) override; void activate(CSVWidget::SceneToolbar*) override; void deactivate(CSVWidget::SceneToolbar*) 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; /// \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; void dragAborted() override; void dragWheel(int diff, double speedFactor) override; void dragMoveEvent(QDragMoveEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; std::shared_ptr getTerrainSelection(); private: /// \brief Handle brush mechanics, maths regarding worldspace hit etc. void editTerrainTextureGrid(const WorldspaceHitResult& hit); /// \brief Check if global selection coordinate belongs to cell in view bool isInCellSelection(int globalSelectionX, int globalSelectionY); /// \brief Handle brush mechanics for texture selection void selectTerrainTextures(const std::pair& texCoords, unsigned char selectMode); /// \brief Push texture edits to command macro void pushEditToCommand(CSMWorld::LandTexturesColumn::DataType& newLandGrid, CSMDoc::Document& document, CSMWorld::IdTable& landTable, std::string cellId); /// \brief Create new cell and land if needed bool allowLandTextureEditing(const std::string& textureFileName); 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 }; signals: void passBrushTexture(ESM::RefId brushTexture); public slots: void handleDropEvent(QDropEvent* event); void setBrushSize(int brushSize); void setBrushShape(CSVWidget::BrushShape brushShape); void setBrushTexture(ESM::RefId brushShape); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/unpagedworldspacewidget.cpp000066400000000000000000000261731503074453300265330ustar00rootroot00000000000000#include "unpagedworldspacewidget.hpp" #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 "../widget/scenetooltoggle2.hpp" #include "cameracontroller.hpp" namespace CSVRender { class TagBase; } namespace osg { class Vec3f; } void CSVRender::UnpagedWorldspaceWidget::update() { const CSMWorld::Record& record = dynamic_cast&>(mCellsModel->getRecord(mCellId)); osg::Vec4f colour = SceneUtil::colourFromRGB(record.get().mAmbi.mAmbient); setDefaultAmbient(colour); bool isInterior = (record.get().mData.mFlags & ESM::Cell::Interior) != 0; bool behaveLikeExterior = (record.get().mData.mFlags & ESM::Cell::QuasiEx) != 0; setExterior(behaveLikeExterior || !isInterior); /// \todo deal with mSunlight and mFog/mForDensity flagAsModified(); } 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)); mReferenceablesModel = &dynamic_cast( *document.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables)); connect(mCellsModel, &CSMWorld::IdTable::dataChanged, this, &UnpagedWorldspaceWidget::cellDataChanged); connect(mCellsModel, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved); connect( &document.getData(), &CSMWorld::Data::assetTablesChanged, this, &UnpagedWorldspaceWidget::assetTablesChanged); update(); mCell = std::make_unique(document, mRootNode, mCellId); } void CSVRender::UnpagedWorldspaceWidget::cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { int index = mCellsModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification); QModelIndex cellIndex = mCellsModel->getModelIndex(mCellId, index); if (cellIndex.row() >= topLeft.row() && cellIndex.row() <= bottomRight.row()) { if (mCellsModel->data(cellIndex).toInt() == CSMWorld::RecordBase::State_Deleted) { emit closeRequest(); } else { /// \todo possible optimisation: check columns and update only if relevant columns have /// changed update(); } } } void CSVRender::UnpagedWorldspaceWidget::cellRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { QModelIndex cellIndex = mCellsModel->getModelIndex(mCellId, 0); if (cellIndex.row() >= start && cellIndex.row() <= end) emit closeRequest(); } void CSVRender::UnpagedWorldspaceWidget::assetTablesChanged() { if (mCell) mCell->reloadAssets(); } bool CSVRender::UnpagedWorldspaceWidget::handleDrop( const std::vector& universalIdData, DropType type) { if (WorldspaceWidget::handleDrop(universalIdData, type)) return true; if (type != Type_CellsInterior) return false; mCellId = universalIdData.begin()->getId(); mCell = std::make_unique(getDocument(), mRootNode, mCellId); mCamPositionSet = false; mOrbitCamControl->reset(); update(); emit cellChanged(*universalIdData.begin()); return true; } void CSVRender::UnpagedWorldspaceWidget::clearSelection(int elementMask) { mCell->setSelection(elementMask, Cell::Selection_Clear); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::invertSelection(int elementMask) { mCell->setSelection(elementMask, Cell::Selection_Invert); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectAll(int elementMask) { mCell->setSelection(elementMask, Cell::Selection_All); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectAllWithSameParentId(int elementMask) { mCell->selectAllWithSameParentId(elementMask); flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::selectInsideCube( const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) { mCell->selectInsideCube(pointA, pointB, dragMode); } void CSVRender::UnpagedWorldspaceWidget::selectWithinDistance( const osg::Vec3d& point, float distance, DragMode dragMode) { mCell->selectWithinDistance(point, distance, dragMode); } std::string CSVRender::UnpagedWorldspaceWidget::getCellId(const osg::Vec3f& point) const { return mCellId; } CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const osg::Vec3d& point) const { return mCell.get(); } CSVRender::Cell* CSVRender::UnpagedWorldspaceWidget::getCell(const CSMWorld::CellCoordinates& coords) const { return mCell.get(); } 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); } void CSVRender::UnpagedWorldspaceWidget::selectGroup(const std::vector& group) const { mCell->selectFromGroup(group); } void CSVRender::UnpagedWorldspaceWidget::unhideAll() const { mCell->unhideAll(); } std::vector> CSVRender::UnpagedWorldspaceWidget::getEdited( unsigned int elementMask) const { return mCell->getEdited(elementMask); } void CSVRender::UnpagedWorldspaceWidget::setSubMode(int subMode, unsigned int elementMask) { mCell->setSubMode(subMode, elementMask); } void CSVRender::UnpagedWorldspaceWidget::reset(unsigned int elementMask) { mCell->reset(elementMask); } void CSVRender::UnpagedWorldspaceWidget::referenceableDataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) if (mCell.get()->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceableAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceableAboutToBeRemoved(parent, start, end)) flagAsModified(); } 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()); if (mCell.get()->referenceableDataChanged(topLeft, bottomRight)) flagAsModified(); } } void CSVRender::UnpagedWorldspaceWidget::referenceDataChanged( const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mCell.get()) if (mCell.get()->referenceDataChanged(topLeft, bottomRight)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceAboutToBeRemoved(parent, start, end)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::referenceAdded(const QModelIndex& parent, int start, int end) { if (mCell.get()) if (mCell.get()->referenceAdded(parent, start, end)) flagAsModified(); } void CSVRender::UnpagedWorldspaceWidget::pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); int rowStart = -1; int rowEnd = -1; if (topLeft.parent().isValid()) { rowStart = topLeft.parent().row(); rowEnd = bottomRight.parent().row(); } else { rowStart = topLeft.row(); rowEnd = bottomRight.row(); } for (int row = rowStart; row <= rowEnd; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); return; } } } void CSVRender::UnpagedWorldspaceWidget::pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { // Pathgrid going to be deleted for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridRemoved(); flagAsModified(); return; } } } } void CSVRender::UnpagedWorldspaceWidget::pathgridAdded(const QModelIndex& parent, int start, int end) { const CSMWorld::SubCellCollection& pathgrids = mDocument.getData().getPathgrids(); if (!parent.isValid()) { for (int row = start; row <= end; ++row) { const CSMWorld::Pathgrid& pathgrid = pathgrids.getRecord(row).get(); if (ESM::RefId::stringRefId(mCellId) == pathgrid.mId) { mCell->pathgridModified(); flagAsModified(); return; } } } } void CSVRender::UnpagedWorldspaceWidget::addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) { WorldspaceWidget::addVisibilitySelectorButtons(tool); tool->addButton(Button_Terrain, Mask_Terrain, "Terrain", "", true); } std::string CSVRender::UnpagedWorldspaceWidget::getStartupInstruction() { osg::Vec3d eye, center, up; mView->getCamera()->getViewMatrixAsLookAt(eye, center, up); osg::Vec3d position = eye; std::ostringstream stream; 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 { dropRequirments requirements = WorldspaceWidget::getDropRequirements(type); if (requirements != ignored) return requirements; switch (type) { case Type_CellsInterior: return canHandle; case Type_CellsExterior: return needPaged; default: return ignored; } } openmw-openmw-0.49.0/apps/opencs/view/render/unpagedworldspacewidget.hpp000066400000000000000000000104471503074453300265350ustar00rootroot00000000000000#ifndef OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #define OPENCS_VIEW_UNPAGEDWORLDSPACEWIDGET_H #include #include #include #include #include #include #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; class CellCoordinates; } namespace CSVRender { class TagBase; class UnpagedWorldspaceWidget : public WorldspaceWidget { Q_OBJECT CSMDoc::Document& mDocument; std::string mCellId; CSMWorld::IdTable* mCellsModel; CSMWorld::IdTable* mReferenceablesModel; std::unique_ptr mCell; void update(); public: UnpagedWorldspaceWidget(const std::string& cellId, CSMDoc::Document& document, QWidget* parent); dropRequirments getDropRequirements(DropType type) const 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 select operation void invertSelection(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; void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) override; void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) override; std::string getCellId(const osg::Vec3f& point) const override; Cell* getCell(const osg::Vec3d& point) 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; void selectGroup(const std::vector& group) const override; void unhideAll() const override; std::vector> getEdited(unsigned int elementMask) const override; void setSubMode(int subMode, unsigned int elementMask) override; /// Erase all overrides and restore the visual representation to its true state. void reset(unsigned int elementMask) override; private: void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) override; void referenceableAboutToBeRemoved(const QModelIndex& parent, 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 referenceAboutToBeRemoved(const QModelIndex& parent, 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 pathgridAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void pathgridAdded(const QModelIndex& parent, int start, int end) override; std::string getStartupInstruction() override; protected: void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool) override; private slots: void cellDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void cellRowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); void assetTablesChanged(); signals: void cellChanged(const CSMWorld::UniversalId& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/render/worldspacewidget.cpp000066400000000000000000000727671503074453300252010ustar00rootroot00000000000000#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 "../../model/world/idtable.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "../render/orbitcameramode.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetoolrun.hpp" #include "../widget/scenetooltoggle2.hpp" #include "cameracontroller.hpp" #include "instancemode.hpp" #include "mask.hpp" #include "object.hpp" #include "pathgridmode.hpp" 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) , mDragMode(InteractionType_None) , mDragging(false) , mDragX(0) , mDragY(0) , mSpeedMode(false) , mDragFactor(0) , mDragWheelFactor(0) , mDragShiftFactor(0) , mToolTipPos(-1, -1) , mShowToolTips(false) , mToolTipDelay(0) , mInConstructor(true) , mSelectedNavigationMode(0) { setAcceptDrops(true); QAbstractItemModel* referenceables = document.getData().getTableModel(CSMWorld::UniversalId::Type_Referenceables); 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); 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); 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); connect(debugProfiles, &QAbstractItemModel::dataChanged, this, &WorldspaceWidget::debugProfileDataChanged); connect(debugProfiles, &QAbstractItemModel::rowsAboutToBeRemoved, this, &WorldspaceWidget::debugProfileAboutToBeRemoved); 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* primaryOpenShortcut = new CSMPrefs::Shortcut("scene-open-primary", this); 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, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::secondaryEdit); CSMPrefs::Shortcut* primarySelectShortcut = new CSMPrefs::Shortcut("scene-select-primary", this); connect( primarySelectShortcut, qOverload(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::primarySelect); CSMPrefs::Shortcut* secondarySelectShortcut = new CSMPrefs::Shortcut("scene-select-secondary", this); 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, qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::abortDrag); connect(new CSMPrefs::Shortcut("scene-toggle-visibility", this), qOverload<>(&CSMPrefs::Shortcut::activated), this, &WorldspaceWidget::toggleHiddenInstances); 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) { if (*setting == "3D Scene Input/drag-factor") mDragFactor = setting->toDouble(); else if (*setting == "3D Scene Input/drag-wheel-factor") mDragWheelFactor = setting->toDouble(); else if (*setting == "3D Scene Input/drag-shift-factor") mDragShiftFactor = setting->toDouble(); else if (*setting == "Rendering/object-marker-alpha" && !mInConstructor) { float alpha = setting->toDouble(); // getSelection is virtual, thus this can not be called from the constructor auto selection = getSelection(Mask_Reference); for (osg::ref_ptr tag : selection) { if (auto objTag = dynamic_cast(tag.get())) objTag->mObject->setMarkerTransparency(alpha); } } else if (*setting == "Tooltips/scene-delay") mToolTipDelay = setting->toInt(); else if (*setting == "Tooltips/scene") mShowToolTips = setting->isTrue(); else SceneWidget::settingChanged(setting); } void CSVRender::WorldspaceWidget::useViewHint(const std::string& hint) {} void CSVRender::WorldspaceWidget::selectDefaultNavigationMode() { selectNavigationMode("1st"); } void CSVRender::WorldspaceWidget::centerOrbitCameraOnSelection() { std::vector> selection = getSelection(Mask_Reference); for (std::vector>::iterator it = selection.begin(); it != selection.end(); ++it) { if (CSVRender::ObjectTag* objectTag = static_cast(it->get())) { mOrbitCamControl->setCenter(objectTag->mObject->getPosition().asVec3()); } } } CSVWidget::SceneToolMode* CSVRender::WorldspaceWidget::makeNavigationSelector(CSVWidget::SceneToolbar* parent) { CSVWidget::SceneToolMode* tool = new CSVWidget::SceneToolMode(parent, "Camera Mode"); /// \todo replace icons /// \todo consider user-defined button-mapping tool->addButton(":scenetoolbar/1st-person", "1st", "First Person" "
  • Camera is held upright
  • " "
  • Mouse-Look while holding {scene-navi-primary}
  • " "
  • Movement keys: {free-forward}(forward), {free-left}(left), {free-backward}(back), {free-right}(right)
  • " "
  • Strafing (also vertically) by holding {scene-navi-secondary}
  • " "
  • Mouse wheel moves the camera forward/backward
  • " "
  • Hold {scene-speed-modifier} to speed up movement
  • " "
"); 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)
  • " "
  • Roll camera with {free-roll-left} and {free-roll-right} keys
  • " "
  • Strafing (also vertically) by holding {scene-navi-secondary}
  • " "
  • Mouse wheel moves the camera forward/backward
  • " "
  • Hold {free-forward:mod} to speed up movement
  • " "
"); tool->addButton( 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}
  • " "
  • 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)
  • " "
  • Mouse wheel moves camera away or towards centre point but can not pass through it
  • " "
  • Hold {scene-speed-modifier} to speed up movement
  • " "
", tool), "orbit"); mCameraMode = tool; connect(mCameraMode, &CSVWidget::SceneToolMode::modeChanged, this, &WorldspaceWidget::selectNavigationMode); return mCameraMode; } CSVWidget::SceneToolToggle2* CSVRender::WorldspaceWidget::makeSceneVisibilitySelector(CSVWidget::SceneToolbar* parent) { mSceneElements = new CSVWidget::SceneToolToggle2( parent, "Scene Element Visibility", ":scenetoolbar/scene-view-c", ":scenetoolbar/scene-view-"); addVisibilitySelectorButtons(mSceneElements); mSceneElements->setSelectionMask(0xffffffff); connect(mSceneElements, &CSVWidget::SceneToolToggle2::selectionChanged, this, &WorldspaceWidget::elementSelectionChanged); return mSceneElements; } CSVWidget::SceneToolRun* CSVRender::WorldspaceWidget::makeRunTool(CSVWidget::SceneToolbar* parent) { 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 size = debugProfiles.rowCount(); for (int i = 0; i < size; ++i) { int state = debugProfiles.data(debugProfiles.index(i, stateColumn)).toInt(); bool default_ = debugProfiles.data(debugProfiles.index(i, defaultColumn)).toInt(); if (state != CSMWorld::RecordBase::State_Deleted && default_) profiles.emplace_back(debugProfiles.data(debugProfiles.index(i, idColumn)).toString().toUtf8().constData()); } std::sort(profiles.begin(), profiles.end()); mRun = new CSVWidget::SceneToolRun( parent, "Run OpenMW from the current camera position", ":scenetoolbar/play", profiles); connect(mRun, &CSVWidget::SceneToolRun::runRequest, this, &WorldspaceWidget::runRequest); return mRun; } CSVWidget::SceneToolMode* CSVRender::WorldspaceWidget::makeEditModeSelector(CSVWidget::SceneToolbar* parent) { mEditMode = new CSVWidget::SceneToolMode(parent, "Edit Mode"); addEditModeSelectorButtons(mEditMode); connect(mEditMode, &CSVWidget::SceneToolMode::modeChanged, this, &WorldspaceWidget::editModeChanged); return mEditMode; } 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) { DropType type = Type_Other; if (iter->getType() == CSMWorld::UniversalId::Type_Cell || iter->getType() == CSMWorld::UniversalId::Type_Cell_Missing) { type = iter->getId()[0] == '#' ? Type_CellsExterior : Type_CellsInterior; } else if (iter->getType() == CSMWorld::UniversalId::Type_DebugProfile) type = Type_DebugProfile; if (iter == data.begin()) output = type; else if (output != type) // mixed types -> ignore return Type_Other; } return output; } CSVRender::WorldspaceWidget::dropRequirments CSVRender::WorldspaceWidget::getDropRequirements(DropType type) const { if (type == Type_DebugProfile) return canHandle; return ignored; } bool CSVRender::WorldspaceWidget::handleDrop(const std::vector& universalIdData, DropType type) { if (type == Type_DebugProfile) { if (mRun) { for (std::vector::const_iterator iter(universalIdData.begin()); iter != universalIdData.end(); ++iter) mRun->addProfile(iter->getId()); } return true; } return false; } unsigned int CSVRender::WorldspaceWidget::getVisibilityMask() const { return mSceneElements->getSelectionMask(); } void CSVRender::WorldspaceWidget::setInteractionMask(unsigned int mask) { mInteractionMask = mask | Mask_CellMarker | Mask_CellArrow; } unsigned int CSVRender::WorldspaceWidget::getInteractionMask() const { return mInteractionMask & getVisibilityMask(); } void CSVRender::WorldspaceWidget::setEditLock(bool locked) { dynamic_cast(*mEditMode->getCurrent()).setEditLock(locked); } 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"); } 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"); } CSMDoc::Document& CSVRender::WorldspaceWidget::getDocument() { return mDocument; } 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() * 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); 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)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::NO_LIMIT); osgUtil::IntersectionVisitor visitor(intersector); visitor.setTraversalMask(interactionMask); mView->getCamera()->accept(visitor); // Get relevant data for (osgUtil::LineSegmentIntersector::Intersections::iterator it = intersector->getIntersections().begin(); it != intersector->getIntersections().end(); ++it) { osgUtil::LineSegmentIntersector::Intersection intersection = *it; // reject back-facing polygons if (direction * intersection.getWorldIntersectNormal() > 0) { continue; } for (std::vector::iterator nodeIter = intersection.nodePath.begin(); nodeIter != intersection.nodePath.end(); ++nodeIter) { osg::Node* node = *nodeIter; if (osg::ref_ptr tag = dynamic_cast(node->getUserData())) { WorldspaceHitResult hit = { true, std::move(tag), 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; hit.index1 = intersection.indexList[1]; hit.index2 = intersection.indexList[2]; } return hit; } } // Something untagged, probably terrain WorldspaceHitResult hit = { true, nullptr, 0, 0, 0, intersection.getWorldIntersectPoint() }; if (intersection.indexList.size() >= 3) { hit.index0 = intersection.indexList[0]; hit.index1 = intersection.indexList[1]; hit.index2 = intersection.indexList[2]; } return hit; } // Default placement direction.normalize(); direction *= CSMPrefs::get()["3D Scene Editing"]["distance"].toInt(); WorldspaceHitResult hit = { false, nullptr, 0, 0, 0, start + direction }; 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.dragAborted(); mDragMode = InteractionType_None; } } void CSVRender::WorldspaceWidget::dragEnterEvent(QDragEnterEvent* 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->fromDocument(mDocument)) { 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); } } void CSVRender::WorldspaceWidget::dragMoveEvent(QDragMoveEvent* 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->fromDocument(mDocument)) { 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); } } void CSVRender::WorldspaceWidget::dropEvent(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->fromDocument(mDocument)) { 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); } } void CSVRender::WorldspaceWidget::runRequest(const std::string& profile) { mDocument.startRunning(profile, getStartupInstruction()); } 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)); 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) { 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()); } } void CSVRender::WorldspaceWidget::debugProfileAboutToBeRemoved(const QModelIndex& parent, int start, int end) { if (parent.isValid()) return; if (!mRun) return; CSMWorld::IdTable& debugProfiles = dynamic_cast( *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_DebugProfiles)); int idColumn = debugProfiles.findColumnIndex(CSMWorld::Columns::ColumnId_Id); for (int i = start; i <= end; ++i) { mRun->removeProfile(debugProfiles.data(debugProfiles.index(i, idColumn)).toString().toUtf8().constData()); } } void CSVRender::WorldspaceWidget::editModeChanged(const std::string& id) { dynamic_cast(*mEditMode->getCurrent()).setEditLock(mLocked); mDragging = false; mDragMode = InteractionType_None; } void CSVRender::WorldspaceWidget::showToolTip() { if (mShowToolTips) { QPoint pos = QCursor::pos(); 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, hit), this); } } } void CSVRender::WorldspaceWidget::elementSelectionChanged() { setVisibilityMask(getVisibilityMask()); flagAsModified(); updateOverlay(); } void CSVRender::WorldspaceWidget::updateOverlay() {} void CSVRender::WorldspaceWidget::mouseMoveEvent(QMouseEvent* event) { dynamic_cast(*mEditMode->getCurrent()).mouseMoveEvent(event); if (mDragging) { int diffX = event->x() - mDragX; int diffY = (height() - event->y()) - mDragY; mDragX = event->x(); mDragY = height() - event->y(); double factor = mDragFactor; if (mSpeedMode) factor *= mDragShiftFactor; EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); editMode.drag(event->pos(), diffX, diffY, factor); } else if (mDragMode != InteractionType_None) { EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (mDragMode == InteractionType_PrimaryEdit) mDragging = editMode.primaryEditStartDrag(event->pos()); else if (mDragMode == InteractionType_SecondaryEdit) mDragging = editMode.secondaryEditStartDrag(event->pos()); else if (mDragMode == InteractionType_PrimarySelect) mDragging = editMode.primarySelectStartDrag(event->pos()); else if (mDragMode == InteractionType_SecondarySelect) mDragging = editMode.secondarySelectStartDrag(event->pos()); if (mDragging) { mDragX = event->localPos().x(); mDragY = height() - event->localPos().y(); } } else { if (event->globalPos() != mToolTipPos) { mToolTipPos = event->globalPos(); if (mShowToolTips) { QToolTip::hideText(); mToolTipDelayTimer.start(mToolTipDelay); } } SceneWidget::mouseMoveEvent(event); } } void CSVRender::WorldspaceWidget::wheelEvent(QWheelEvent* event) { if (mDragging) { double factor = mDragWheelFactor; if (mSpeedMode) factor *= mDragShiftFactor; 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) { EditMode& editMode = dynamic_cast(*mEditMode->getCurrent()); if (type == InteractionType_PrimaryEdit) editMode.primaryEditPressed(hit); else if (type == InteractionType_SecondaryEdit) editMode.secondaryEditPressed(hit); else if (type == InteractionType_PrimarySelect) editMode.primarySelectPressed(hit); else if (type == InteractionType_SecondarySelect) editMode.secondarySelectPressed(hit); else if (type == InteractionType_TertiarySelect) editMode.tertiarySelectPressed(hit); else if (type == InteractionType_PrimaryOpen) editMode.primaryOpenPressed(hit); } void CSVRender::WorldspaceWidget::primaryOpen(bool activate) { handleInteraction(InteractionType_PrimaryOpen, activate); } void CSVRender::WorldspaceWidget::primaryEdit(bool activate) { handleInteraction(InteractionType_PrimaryEdit, activate); } void CSVRender::WorldspaceWidget::secondaryEdit(bool activate) { handleInteraction(InteractionType_SecondaryEdit, activate); } void CSVRender::WorldspaceWidget::primarySelect(bool activate) { handleInteraction(InteractionType_PrimarySelect, activate); } 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) { if (!mDragging) mDragMode = type; } else { mDragMode = InteractionType_None; if (mDragging) { EditMode* editMode = getEditMode(); editMode->dragCompleted(mapFromGlobal(QCursor::pos())); mDragging = false; } else { WorldspaceHitResult hit = mousePick(mapFromGlobal(QCursor::pos()), getInteractionMask()); handleInteractionPress(hit, type); } } } openmw-openmw-0.49.0/apps/opencs/view/render/worldspacewidget.hpp000066400000000000000000000241551503074453300251720ustar00rootroot00000000000000#ifndef OPENCS_VIEW_WORLDSPACEWIDGET_H #define OPENCS_VIEW_WORLDSPACEWIDGET_H #include #include #include #include #include #include #include #include "instancedragmodes.hpp" #include "scenewidget.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 { class Setting; } namespace CSMWorld { class CellCoordinates; class UniversalId; } namespace CSVWidget { class SceneToolMode; class SceneToolToggle2; class SceneToolbar; class SceneToolRun; } namespace CSVRender { class Cell; class EditMode; struct WorldspaceHitResult { bool hit; osg::ref_ptr tag; unsigned int index0, index1, index2; // indices of mesh vertices osg::Vec3d worldPos; }; class WorldspaceWidget : public SceneWidget { 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); /// \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); void selectDefaultNavigationMode(); void centerOrbitCameraOnSelection(); static DropType getDropType(const std::vector& data); virtual dropRequirments getDropRequirements(DropType type) const; virtual void useViewHint(const std::string& hint); ///< Default-implementation: ignored. /// \return Drop handled? virtual bool handleDrop(const std::vector& data, DropType type); virtual unsigned int getVisibilityMask() const; /// \note This function will implicitly add elements that are independent of the /// selected edit mode. virtual void setInteractionMask(unsigned int mask); /// \note This function will only return those elements that are both visible and /// marked for interaction. unsigned int getInteractionMask() const; virtual void setEditLock(bool locked); CSMDoc::Document& getDocument(); /// \param elementMask Elements to be affected by the clear operation virtual void clearSelection(int elementMask) = 0; /// \param elementMask Elements to be affected by the select operation virtual void invertSelection(int elementMask) = 0; /// \param elementMask Elements to be affected by the select operation virtual void selectAll(int elementMask) = 0; // 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 void selectInsideCube(const osg::Vec3d& pointA, const osg::Vec3d& pointB, DragMode dragMode) = 0; virtual void selectWithinDistance(const osg::Vec3d& point, float distance, DragMode dragMode) = 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 std::string getCellId(const osg::Vec3f& point) const = 0; /// \note Returns the cell if it exists, otherwise a null pointer virtual Cell* getCell(const osg::Vec3d& point) const = 0; virtual Cell* getCell(const CSMWorld::CellCoordinates& coords) const = 0; virtual osg::ref_ptr getSnapTarget(unsigned int elementMask) const = 0; virtual std::vector> getSelection(unsigned int elementMask) const = 0; virtual void selectGroup(const std::vector&) const = 0; virtual void unhideAll() const = 0; virtual std::vector> getEdited(unsigned int elementMask) const = 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; EditMode* getEditMode(); 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 }; enum CameraMode { FirstPerson, Orbit, Free }; virtual void addVisibilitySelectorButtons(CSVWidget::SceneToolToggle2* tool); virtual void addEditModeSelectorButtons(CSVWidget::SceneToolMode* tool); virtual void updateOverlay(); void mouseMoveEvent(QMouseEvent* event) override; void wheelEvent(QWheelEvent* event) override; virtual void handleInteractionPress(const WorldspaceHitResult& hit, InteractionType type); void settingChanged(const CSMPrefs::Setting* setting) override; bool getSpeedMode(); void cycleNavigationMode(); private: void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; virtual std::string getStartupInstruction() = 0; void handleInteraction(InteractionType type, bool activate); 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(); private slots: virtual void referenceableDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 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 referenceDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 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 pathgridDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) = 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 runRequest(const std::string& profile); void debugProfileDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void debugProfileAboutToBeRemoved(const QModelIndex& parent, int start, int end); void editModeChanged(const std::string& id); void showToolTip(); void primaryOpen(bool activate); void primaryEdit(bool activate); void secondaryEdit(bool activate); void primarySelect(bool activate); void secondarySelect(bool activate); void tertiarySelect(bool activate); void speedMode(bool activate); void toggleHiddenInstances(); protected slots: void elementSelectionChanged(); signals: void closeRequest(); void dataDropped(const std::vector& data); void requestFocus(const std::string& id); friend class MouseState; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/000077500000000000000000000000001503074453300207645ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/tools/merge.cpp000066400000000000000000000073711503074453300225770ustar00rootroot00000000000000 #include "merge.hpp" #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/adjusterwidget.hpp" #include "../doc/filewidget.hpp" void CSVTools::Merge::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { event->accept(); cancel(); } else QWidget::keyPressEvent(event); } CSVTools::Merge::Merge(CSMDoc::DocumentManager& documentManager, QWidget* parent) : QWidget(parent) , mDocument(nullptr) , mDocumentManager(documentManager) { setWindowTitle("Merge Content Files into a new Game File"); QVBoxLayout* mainLayout = new QVBoxLayout; setLayout(mainLayout); QSplitter* splitter = new QSplitter(Qt::Horizontal, this); mainLayout->addWidget(splitter, 1); // left panel (files to be merged) QWidget* left = new QWidget(this); left->setContentsMargins(0, 0, 0, 0); splitter->addWidget(left); QVBoxLayout* leftLayout = new QVBoxLayout; left->setLayout(leftLayout); leftLayout->addWidget(new QLabel("Files to be merged", this)); 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); QVBoxLayout* rightLayout = new QVBoxLayout; rightLayout->setAlignment(Qt::AlignTop); right->setLayout(rightLayout); rightLayout->addWidget(new QLabel("New game file", this)); mNewFile = new CSVDoc::FileWidget(this); mNewFile->setType(false); mNewFile->extensionLabelIsVisible(true); rightLayout->addWidget(mNewFile); mAdjuster = new CSVDoc::AdjusterWidget(this); rightLayout->addWidget(mAdjuster); 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); connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &Merge::cancel); mOkay = new QPushButton("Merge", this); connect(mOkay, &QPushButton::clicked, this, &Merge::accept); mOkay->setDefault(true); buttons->addButton(mOkay, QDialogButtonBox::AcceptRole); mainLayout->addWidget(buttons); } void CSVTools::Merge::configure(CSMDoc::Document* document) { mDocument = document; mNewFile->setName(""); // content files while (mFiles->count()) delete mFiles->takeItem(0); std::vector files = document->getContentFiles(); for (std::vector::const_iterator iter(files.begin()); iter != files.end(); ++iter) mFiles->addItem(Files::pathToQString(iter->filename())); } void CSVTools::Merge::setLocalData(const std::filesystem::path& localData) { mAdjuster->setLocalData(localData); } CSMDoc::Document* CSVTools::Merge::getDocument() const { return mDocument; } void CSVTools::Merge::cancel() { mDocument = nullptr; hide(); } void CSVTools::Merge::accept() { if ((mDocument->getState() & CSMDoc::State_Merging) == 0) { std::vector files{ mAdjuster->getPath() }; std::unique_ptr target(mDocumentManager.makeDocument(files, files[0], true)); mDocument->runMerge(std::move(target)); hide(); } } void CSVTools::Merge::stateChanged(bool valid) { mOkay->setEnabled(valid); } openmw-openmw-0.49.0/apps/opencs/view/tools/merge.hpp000066400000000000000000000021031503074453300225700ustar00rootroot00000000000000#ifndef CSV_TOOLS_MERGE_H #define CSV_TOOLS_MERGE_H #include #include class QPushButton; class QListWidget; namespace CSMDoc { class Document; class DocumentManager; } namespace CSVDoc { class FileWidget; class AdjusterWidget; } namespace CSVTools { class Merge : public QWidget { Q_OBJECT CSMDoc::Document* mDocument; QPushButton* mOkay; QListWidget* mFiles; CSVDoc::FileWidget* mNewFile; CSVDoc::AdjusterWidget* mAdjuster; CSMDoc::DocumentManager& mDocumentManager; void keyPressEvent(QKeyEvent* event) override; public: Merge(CSMDoc::DocumentManager& documentManager, QWidget* parent = nullptr); /// Configure dialogue for a new merge void configure(CSMDoc::Document* document); void setLocalData(const std::filesystem::path& localData); CSMDoc::Document* getDocument() const; public slots: void cancel(); private slots: void accept(); void stateChanged(bool valid); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/reportsubview.cpp000066400000000000000000000023771503074453300244210ustar00rootroot00000000000000#include "reportsubview.hpp" #include "reporttable.hpp" #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) mRefreshState = CSMDoc::State_Verifying; setWidget(mTable = new ReportTable(document, id, false, mRefreshState, this)); connect(mTable, &ReportTable::editRequest, this, &ReportSubView::focusId); if (mRefreshState == CSMDoc::State_Verifying) { connect(mTable, &ReportTable::refreshRequest, this, &ReportSubView::refreshRequest); connect(&document, &CSMDoc::Document::stateChanged, mTable, &ReportTable::stateChanged); } } void CSVTools::ReportSubView::setEditLock(bool locked) { // ignored. We don't change document state anyway. } void CSVTools::ReportSubView::refreshRequest() { if (!(mDocument.getState() & mRefreshState)) { if (mRefreshState == CSMDoc::State_Verifying) { mTable->clear(); mDocument.verify(getUniversalId()); } } } openmw-openmw-0.49.0/apps/opencs/view/tools/reportsubview.hpp000066400000000000000000000011731503074453300244170ustar00rootroot00000000000000#ifndef CSV_TOOLS_REPORTSUBVIEW_H #define CSV_TOOLS_REPORTSUBVIEW_H #include "../doc/subview.hpp" #include class QObject; namespace CSMDoc { class Document; } namespace CSVTools { class ReportTable; class ReportSubView : public CSVDoc::SubView { Q_OBJECT ReportTable* mTable; CSMDoc::Document& mDocument; int mRefreshState; public: ReportSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; private slots: void refreshRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/reporttable.cpp000066400000000000000000000243521503074453300240210ustar00rootroot00000000000000#include "reporttable.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/tools/reportmodel.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "../../view/world/idtypedelegate.hpp" namespace CSVTools { class RichTextDelegate : public QStyledItemDelegate { public: RichTextDelegate(QObject* parent = nullptr); void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; } CSVTools::RichTextDelegate::RichTextDelegate(QObject* parent) : QStyledItemDelegate(parent) { } void CSVTools::RichTextDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { QTextDocument document; 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()); } } void CSVTools::ReportTable::contextMenuEvent(QContextMenuEvent* event) { QModelIndexList selectedRows = selectionModel()->selectedRows(); // create context menu QMenu menu(this); if (!selectedRows.empty()) { menu.addAction(mShowAction); menu.addAction(mRemoveAction); bool found = false; for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { QString hint = mProxyModel->data(mProxyModel->index(iter->row(), 2)).toString(); if (!hint.isEmpty() && hint[0] == 'R') { found = true; break; } } if (found) menu.addAction(mReplaceAction); } if (mRefreshAction) menu.addAction(mRefreshAction); menu.exec(event->globalPos()); } void CSVTools::ReportTable::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) startDragFromTable(*this, indexAt(event->pos())); } void CSVTools::ReportTable::mouseDoubleClickEvent(QMouseEvent* event) { Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); selectionModel()->select( index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); std::map::iterator iter = mDoubleClickActions.find(modifiers); if (iter == mDoubleClickActions.end()) { event->accept(); return; } switch (iter->second) { case Action_None: event->accept(); break; case Action_Edit: event->accept(); showSelection(); break; case Action_Remove: event->accept(); removeSelection(); break; case Action_EditAndRemove: event->accept(); showSelection(); removeSelection(); break; } } 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); verticalHeader()->hide(); setSortingEnabled(true); setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::ExtendedSelection); mProxyModel = new QSortFilterProxyModel(this); mProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); mProxyModel->setSourceModel(mModel); mProxyModel->setSortRole(Qt::UserRole); setModel(mProxyModel); setColumnHidden(2, true); mIdTypeDelegate = CSVWorld::IdTypeDelegateFactory().makeDelegate(nullptr, mDocument, this); setItemDelegateForColumn(0, mIdTypeDelegate); if (richTextDescription) setItemDelegateForColumn(mModel->columnCount() - 1, new RichTextDelegate(this)); 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, &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, &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, &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)); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ReportTable::settingChanged); CSMPrefs::get()["Reports"].update(); } std::vector CSVTools::ReportTable::getDraggedRecords() const { std::vector ids; QModelIndexList selectedRows = selectionModel()->selectedRows(); for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { ids.push_back(mModel->getUniversalId(mProxyModel->mapToSource(*iter).row())); } return ids; } std::vector CSVTools::ReportTable::getReplaceIndices(bool selection) const { std::vector indices; if (selection) { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector rows; for (QModelIndexList::const_iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { rows.push_back(mProxyModel->mapToSource(*iter).row()); } std::sort(rows.begin(), rows.end()); for (std::vector::const_iterator iter(rows.begin()); iter != rows.end(); ++iter) { QString hint = mModel->data(mModel->index(*iter, 2)).toString(); if (!hint.isEmpty() && hint[0] == 'R') indices.push_back(*iter); } } else { for (int i = 0; i < mModel->rowCount(); ++i) { QString hint = mModel->data(mModel->index(i, 2)).toString(); if (!hint.isEmpty() && hint[0] == 'R') indices.push_back(i); } } return indices; } void CSVTools::ReportTable::flagAsReplaced(int index) { mModel->flagAsReplaced(index); } void CSVTools::ReportTable::settingChanged(const CSMPrefs::Setting* setting) { if (setting->getParent()->getKey() == "Reports") { QString base("double"); QString key = setting->getKey().c_str(); if (key.startsWith(base)) { QString modifierString = key.mid(base.size()); Qt::KeyboardModifiers modifiers; if (modifierString == "-s") modifiers = Qt::ShiftModifier; else if (modifierString == "-c") modifiers = Qt::ControlModifier; else if (modifierString == "-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); if (value == "Edit") action = Action_Edit; else if (value == "Remove") action = Action_Remove; else if (value == "Edit And Remove") action = Action_EditAndRemove; mDoubleClickActions[modifiers] = action; return; } } 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) { int row = mProxyModel->mapToSource(*iter).row(); emit editRequest(mModel->getUniversalId(row), mModel->getHint(row)); } } void CSVTools::ReportTable::removeSelection() { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector rows; for (QModelIndexList::iterator iter(selectedRows.begin()); iter != selectedRows.end(); ++iter) { rows.push_back(mProxyModel->mapToSource(*iter).row()); } std::sort(rows.begin(), rows.end()); for (std::vector::const_reverse_iterator iter(rows.rbegin()); iter != rows.rend(); ++iter) mProxyModel->removeRows(*iter, 1); selectionModel()->clear(); } void CSVTools::ReportTable::clear() { mModel->clear(); } void CSVTools::ReportTable::stateChanged(int state, CSMDoc::Document* document) { if (mRefreshAction) mRefreshAction->setEnabled(!(state & mRefreshState)); } openmw-openmw-0.49.0/apps/opencs/view/tools/reporttable.hpp000066400000000000000000000052271503074453300240260ustar00rootroot00000000000000#ifndef CSV_TOOLS_REPORTTABLE_H #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 { class ReportModel; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class CommandDelegate; } 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; private: void contextMenuEvent(QContextMenuEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; 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; 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; /// \param index row in the original model void flagAsReplaced(int index); private slots: void settingChanged(const CSMPrefs::Setting* setting); void showSelection(); void removeSelection(); public slots: void stateChanged(int state, CSMDoc::Document* document); signals: void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); void replaceRequest(); void refreshRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/searchbox.cpp000066400000000000000000000120561503074453300234520ustar00rootroot00000000000000#include "searchbox.hpp" #include #include #include "../../model/world/columns.hpp" #include "../../model/tools/search.hpp" void CSVTools::SearchBox::updateSearchButton() { if (!mSearchEnabled) mSearch.setEnabled(false); else { switch (mMode.currentIndex()) { case 0: case 1: case 2: case 3: mSearch.setEnabled(!mText.text().isEmpty()); break; case 4: mSearch.setEnabled(true); break; } } } CSVTools::SearchBox::SearchBox(QWidget* parent) : QWidget(parent) , mSearch(tr("Search")) , mSearchEnabled(false) , mReplace(tr("Replace All")) { 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, 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); mLayout->addWidget(&mReplaceInput, 1, 1); mLayout->addWidget(&mReplace, 1, 3); // layout adjustments mLayout->setColumnMinimumWidth(2, 50); mLayout->setColumnStretch(1, 1); mLayout->setContentsMargins(0, 0, 0, 0); connect(&mReplace, &QPushButton::clicked, this, qOverload(&SearchBox::replaceAll)); // update modeSelected(0); updateSearchButton(); } void CSVTools::SearchBox::setSearchMode(bool enabled) { mSearchEnabled = enabled; updateSearchButton(); } CSMTools::Search CSVTools::SearchBox::getSearch() const { CSMTools::Search::Type type = static_cast(mMode.currentIndex()); bool caseSensitive = mCaseSensitive.isChecked(); switch (type) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_Id: 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, QRegularExpression(mText.text().toUtf8().data(), QRegularExpression::CaseInsensitiveOption)); case CSMTools::Search::Type_RecordState: return CSMTools::Search(type, caseSensitive, mRecordState.currentIndex()); case CSMTools::Search::Type_None: break; } throw std::logic_error("invalid search mode index"); } std::string CSVTools::SearchBox::getReplaceText() const { CSMTools::Search::Type type = static_cast(mMode.currentIndex()); switch (type) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_Id: case CSMTools::Search::Type_IdRegEx: return mReplaceText.text().toUtf8().data(); default: throw std::logic_error("Invalid search mode for replace"); } } void CSVTools::SearchBox::setEditLock(bool locked) { mReplace.setEnabled(!locked); } void CSVTools::SearchBox::focus() { mInput.currentWidget()->setFocus(); } void CSVTools::SearchBox::modeSelected(int index) { switch (index) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_Id: case CSMTools::Search::Type_IdRegEx: mInput.setCurrentIndex(0); mReplaceInput.setCurrentIndex(0); break; case CSMTools::Search::Type_RecordState: mInput.setCurrentIndex(1); mReplaceInput.setCurrentIndex(1); break; } mInput.currentWidget()->setFocus(); updateSearchButton(); } void CSVTools::SearchBox::textChanged(const QString& text) { updateSearchButton(); } void CSVTools::SearchBox::startSearch(bool checked) { if (mSearch.isEnabled()) emit startSearch(getSearch()); } void CSVTools::SearchBox::replaceAll(bool checked) { emit replaceAll(); } openmw-openmw-0.49.0/apps/opencs/view/tools/searchbox.hpp000066400000000000000000000024771503074453300234650ustar00rootroot00000000000000#ifndef CSV_TOOLS_SEARCHBOX_H #define CSV_TOOLS_SEARCHBOX_H #include #include #include #include #include #include #include class QGridLayout; namespace CSMTools { class Search; } 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; private: void updateSearchButton(); public: SearchBox(QWidget* parent = nullptr); void setSearchMode(bool enabled); CSMTools::Search getSearch() const; std::string getReplaceText() const; void setEditLock(bool locked); void focus(); private slots: void modeSelected(int index); void textChanged(const QString& text); void startSearch(bool checked = true); void replaceAll(bool checked); signals: void startSearch(const CSMTools::Search& search); void replaceAll(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/searchsubview.cpp000066400000000000000000000113641503074453300243470ustar00rootroot00000000000000#include "searchsubview.hpp" #include #include "../../model/doc/document.hpp" #include "../../model/doc/state.hpp" #include "../../model/prefs/state.hpp" #include "../../model/tools/reportmodel.hpp" #include "../../model/tools/search.hpp" #include "../../model/world/idtablebase.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) { if (mLocked) return; std::vector indices = mTable->getReplaceIndices(selection); std::string replace = mSearchBox.getReplaceText(); 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; // 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) { const CSMWorld::UniversalId& id = model.getUniversalId(*iter); CSMWorld::UniversalId::Type type = CSMWorld::UniversalId::getParentType(id.getType()); CSMWorld::IdTableBase* table = &dynamic_cast(*mDocument.getData().getTableModel(type)); if (table != currentTable) { search.configure(table); currentTable = table; } std::string hint = model.getHint(*iter); if (search.verify(mDocument, table, id, hint)) { search.replace(mDocument, table, id, hint, replace); mTable->flagAsReplaced(*iter); if (autoDelete) mTable->model()->removeRows(*iter, 1); } } } void CSVTools::SearchSubView::showEvent(QShowEvent* event) { CSVDoc::SubView::showEvent(event); mSearchBox.focus(); } CSVTools::SearchSubView::SearchSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document) : CSVDoc::SubView(id) , mDocument(document) , mLocked(false) { QVBoxLayout* layout = new QVBoxLayout; layout->addWidget(&mSearchBox); layout->addWidget(mTable = new ReportTable(document, id, true), 2); layout->addWidget(mBottom = new CSVWorld::TableBottomBox(CSVWorld::NullCreatorFactory(), document, id, this), 0); QWidget* widget = new QWidget; widget->setLayout(layout); setWidget(widget); stateChanged(document.getState(), &document); connect(mTable, &ReportTable::editRequest, this, &SearchSubView::focusId); connect(mTable, &ReportTable::replaceRequest, this, &SearchSubView::replaceRequest); connect(&document, &CSMDoc::Document::stateChanged, this, &SearchSubView::stateChanged); connect( &mSearchBox, qOverload(&SearchBox::startSearch), this, &SearchSubView::startSearch); connect(&mSearchBox, qOverload<>(&SearchBox::replaceAll), this, &SearchSubView::replaceAllRequest); connect(document.getReport(id), &CSMTools::ReportModel::rowsRemoved, this, &SearchSubView::tableSizeUpdate); connect(document.getReport(id), &CSMTools::ReportModel::rowsInserted, this, &SearchSubView::tableSizeUpdate); connect(&document, &CSMDoc::Document::operationDone, this, &SearchSubView::operationDone); } void CSVTools::SearchSubView::setEditLock(bool locked) { mLocked = locked; mSearchBox.setEditLock(locked); } void CSVTools::SearchSubView::setStatusBar(bool show) { mBottom->setStatusBar(show); } void CSVTools::SearchSubView::stateChanged(int state, CSMDoc::Document* document) { mSearchBox.setSearchMode(!(state & CSMDoc::State_Searching)); } 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()); mTable->clear(); mDocument.runSearch(getUniversalId(), mSearch); } void CSVTools::SearchSubView::replaceRequest() { replace(true); } void CSVTools::SearchSubView::replaceAllRequest() { replace(false); } void CSVTools::SearchSubView::tableSizeUpdate() { mBottom->tableSizeChanged(mDocument.getReport(getUniversalId())->rowCount(), 0, 0); } void CSVTools::SearchSubView::operationDone(int type, bool failed) { if (type == CSMDoc::State_Searching && !failed && !mDocument.getReport(getUniversalId())->rowCount()) { mBottom->setStatusMessage("No Results"); } } openmw-openmw-0.49.0/apps/opencs/view/tools/searchsubview.hpp000066400000000000000000000023451503074453300243530ustar00rootroot00000000000000#ifndef CSV_TOOLS_SEARCHSUBVIEW_H #define CSV_TOOLS_SEARCHSUBVIEW_H #include "../../model/tools/search.hpp" #include "../doc/subview.hpp" #include #include "searchbox.hpp" namespace CSMDoc { class Document; } namespace CSVWorld { class TableBottomBox; } namespace CSVTools { class ReportTable; 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); protected: void showEvent(QShowEvent* event) override; public: SearchSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; void setStatusBar(bool show) override; private slots: void stateChanged(int state, CSMDoc::Document* document); void startSearch(const CSMTools::Search& search); void replaceRequest(); void replaceAllRequest(); void tableSizeUpdate(); void operationDone(int type, bool failed); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/tools/subviews.cpp000066400000000000000000000012221503074453300233340ustar00rootroot00000000000000#include "subviews.hpp" #include "../doc/subviewfactoryimp.hpp" #include #include #include #include "reportsubview.hpp" #include "searchsubview.hpp" 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); } openmw-openmw-0.49.0/apps/opencs/view/tools/subviews.hpp000066400000000000000000000003271503074453300233460ustar00rootroot00000000000000#ifndef CSV_TOOLS_SUBVIEWS_H #define CSV_TOOLS_SUBVIEWS_H namespace CSVDoc { class SubViewFactoryManager; } namespace CSVTools { void addSubViewFactories(CSVDoc::SubViewFactoryManager& manager); } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/000077500000000000000000000000001503074453300211075ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/widget/brushshapes.hpp000066400000000000000000000003551503074453300241520ustar00rootroot00000000000000#ifndef CSV_WIDGET_BRUSHSHAPES_H #define CSV_WIDGET_BRUSHSHAPES_H namespace CSVWidget { enum BrushShape { BrushShape_Point, BrushShape_Square, BrushShape_Circle, BrushShape_Custom }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/coloreditor.cpp000066400000000000000000000066261503074453300241520ustar00rootroot00000000000000#include "coloreditor.hpp" #include #include #include #include "colorpickerpopup.hpp" 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) : ColorEditor(parent, popupOnStart) { setColor(colorInt); } CSVWidget::ColorEditor::ColorEditor(QWidget* parent, const bool popupOnStart) : QPushButton(parent) , mColorPicker(new ColorPickerPopup(this)) , mPopupOnStart(popupOnStart) { connect(this, &ColorEditor::clicked, this, &ColorEditor::showPicker); connect(mColorPicker, &ColorPickerPopup::colorChanged, this, &ColorEditor::pickerColorChanged); } 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); QPainter painter(this); painter.fillRect(coloredRect, mColor); painter.setPen(Qt::black); painter.drawRect(coloredRect); } void CSVWidget::ColorEditor::showEvent(QShowEvent* event) { QPushButton::showEvent(event); if (isVisible() && mPopupOnStart) { setChecked(true); showPicker(); mPopupOnStart = false; } } QColor CSVWidget::ColorEditor::color() const { return mColor; } int CSVWidget::ColorEditor::colorInt() const { return (mColor.blue() << 16) | (mColor.green() << 8) | (mColor.red()); } void CSVWidget::ColorEditor::setColor(const QColor& color) { mColor = color; update(); } void CSVWidget::ColorEditor::setColor(const int colorInt) { // Color RGB values are stored in given integer. // First byte is red, second byte is green, third byte is blue. QColor color = QColor(colorInt & 0xff, (colorInt >> 8) & 0xff, (colorInt >> 16) & 0xff); setColor(color); } void CSVWidget::ColorEditor::showPicker() { mColorPicker->showPicker(calculatePopupPosition(), mColor); emit pickingFinished(); } void CSVWidget::ColorEditor::pickerColorChanged(const QColor& color) { mColor = color; update(); } QPoint CSVWidget::ColorEditor::calculatePopupPosition() { QRect editorGeometry = geometry(); QRect popupGeometry = mColorPicker->geometry(); QRect screenGeometry = QGuiApplication::primaryScreen()->geometry(); // Center the popup horizontally relative to the editor int localPopupX = (editorGeometry.width() - popupGeometry.width()) / 2; // Popup position need to be specified in global coords QPoint popupPosition = mapToGlobal(QPoint(localPopupX, editorGeometry.height())); // Make sure that the popup isn't out of the screen if (popupPosition.x() < screenGeometry.left()) { popupPosition.setX(screenGeometry.left() + 1); } else if (popupPosition.x() + popupGeometry.width() > screenGeometry.right()) { popupPosition.setX(screenGeometry.right() - popupGeometry.width() - 1); } if (popupPosition.y() + popupGeometry.height() > screenGeometry.bottom()) { // Place the popup above the editor popupPosition.setY(popupPosition.y() - popupGeometry.height() - editorGeometry.height() - 1); } return popupPosition; } openmw-openmw-0.49.0/apps/opencs/view/widget/coloreditor.hpp000066400000000000000000000024361503074453300241520ustar00rootroot00000000000000#ifndef CSV_WIDGET_COLOREDITOR_HPP #define CSV_WIDGET_COLOREDITOR_HPP #include class QColor; class QPoint; class QSize; namespace CSVWidget { class ColorPickerPopup; class ColorEditor : public QPushButton { Q_OBJECT QColor mColor; ColorPickerPopup* mColorPicker; bool mPopupOnStart; 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); QColor color() const; /// \return Color RGB value encoded in an int. int colorInt() const; 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); protected: void paintEvent(QPaintEvent* event) override; void showEvent(QShowEvent* event) override; private: ColorEditor(QWidget* parent = nullptr, const bool popupOnStart = false); private slots: void showPicker(); void pickerColorChanged(const QColor& color); signals: void pickingFinished(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/colorpickerpopup.cpp000066400000000000000000000050371503074453300252200ustar00rootroot00000000000000#include "colorpickerpopup.hpp" #include #include #include #include #include #include CSVWidget::ColorPickerPopup::ColorPickerPopup(QWidget* parent) : QFrame(parent) { setWindowFlags(Qt::Popup); 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, &QColorDialog::currentColorChanged, this, &ColorPickerPopup::colorChanged); QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(mColorPicker); layout->setAlignment(Qt::AlignTop | Qt::AlignLeft); layout->setContentsMargins(0, 0, 0, 0); setLayout(layout); setFixedSize(mColorPicker->size()); } void CSVWidget::ColorPickerPopup::showPicker(const QPoint& position, const QColor& initialColor) { QRect geometry = this->geometry(); geometry.moveTo(position); setGeometry(geometry); // Calling getColor() creates a blocking dialog that will continue execution once the user chooses OK or Cancel QColor color = mColorPicker->getColor(initialColor); if (color.isValid()) mColorPicker->setCurrentColor(color); } void CSVWidget::ColorPickerPopup::mousePressEvent(QMouseEvent* event) { QPushButton* button = qobject_cast(parentWidget()); if (button != nullptr) { QStyleOptionButton option; option.initFrom(button); QRect buttonRect = option.rect; buttonRect.moveTo(button->mapToGlobal(buttonRect.topLeft())); // If the mouse is pressed above the pop-up parent, // the pop-up will be hidden and the pressed signal won't be repeated for the parent if (buttonRect.contains(event->globalPos()) || buttonRect.contains(event->pos())) { setAttribute(Qt::WA_NoMouseReplay); } } QFrame::mousePressEvent(event); } bool CSVWidget::ColorPickerPopup::eventFilter(QObject* object, QEvent* event) { if (object == mColorPicker && event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); // Prevent QColorDialog from closing when Escape is pressed. // Instead, hide the popup. if (keyEvent->key() == Qt::Key_Escape) { hide(); return true; } } return QFrame::eventFilter(object, event); } openmw-openmw-0.49.0/apps/opencs/view/widget/colorpickerpopup.hpp000066400000000000000000000011401503074453300252140ustar00rootroot00000000000000#ifndef CSVWIDGET_COLORPICKERPOPUP_HPP #define CSVWIDGET_COLORPICKERPOPUP_HPP #include class QColorDialog; namespace CSVWidget { class ColorPickerPopup : public QFrame { Q_OBJECT QColorDialog* mColorPicker; public: 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; signals: void colorChanged(const QColor& color); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/completerpopup.cpp000066400000000000000000000016271503074453300246770ustar00rootroot00000000000000#include "completerpopup.hpp" CSVWidget::CompleterPopup::CompleterPopup(QWidget* parent) : QListView(parent) { setEditTriggers(QAbstractItemView::NoEditTriggers); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::SingleSelection); } int CSVWidget::CompleterPopup::sizeHintForRow(int row) const { if (model() == nullptr) { return -1; } if (row < 0 || row >= model()->rowCount()) { return -1; } ensurePolished(); QModelIndex index = model()->index(row, modelColumn()); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); #else QStyleOptionViewItem option = viewOptions(); #endif QAbstractItemDelegate* delegate = itemDelegate(index); return delegate->sizeHint(option, index).height(); } openmw-openmw-0.49.0/apps/opencs/view/widget/completerpopup.hpp000066400000000000000000000004601503074453300246760ustar00rootroot00000000000000#ifndef CSV_WIDGET_COMPLETERPOPUP_HPP #define CSV_WIDGET_COMPLETERPOPUP_HPP #include namespace CSVWidget { class CompleterPopup : public QListView { public: CompleterPopup(QWidget* parent = nullptr); int sizeHintForRow(int row) const override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/droplineedit.cpp000066400000000000000000000022501503074453300242740ustar00rootroot00000000000000#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) { setAcceptDrops(true); } void CSVWidget::DropLineEdit::dragEnterEvent(QDragEnterEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { event->acceptProposedAction(); } } void CSVWidget::DropLineEdit::dragMoveEvent(QDragMoveEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { event->accept(); } } void CSVWidget::DropLineEdit::dropEvent(QDropEvent* event) { if (CSVWorld::DragDropUtils::canAcceptData(*event, mDropType)) { CSMWorld::UniversalId id = CSVWorld::DragDropUtils::getAcceptedData(*event, mDropType); setText(QString::fromUtf8(id.getId().c_str())); emit tableMimeDataDropped(id, CSVWorld::DragDropUtils::getTableMimeData(*event)->getDocumentPtr()); } } openmw-openmw-0.49.0/apps/opencs/view/widget/droplineedit.hpp000066400000000000000000000015151503074453300243040ustar00rootroot00000000000000#ifndef CSV_WIDGET_DROPLINEEDIT_HPP #define CSV_WIDGET_DROPLINEEDIT_HPP #include #include "../../model/world/columnbase.hpp" namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } namespace CSVWidget { class DropLineEdit : public QLineEdit { Q_OBJECT CSMWorld::ColumnBase::Display mDropType; ///< The accepted Display type for this LineEdit. 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; signals: void tableMimeDataDropped(const CSMWorld::UniversalId& id, const CSMDoc::Document* document); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/modebutton.cpp000066400000000000000000000007071503074453300237770ustar00rootroot00000000000000#include "modebutton.hpp" #include class QWidget; 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) { return false; } openmw-openmw-0.49.0/apps/opencs/view/widget/modebutton.hpp000066400000000000000000000017341503074453300240050ustar00rootroot00000000000000#ifndef CSV_WIDGET_MODEBUTTON_H #define CSV_WIDGET_MODEBUTTON_H #include "pushbutton.hpp" class QMenu; namespace CSVWidget { class SceneToolbar; /// \brief Specialist PushButton of Type_Mode for use in SceneToolMode class ModeButton : public PushButton { Q_OBJECT public: 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 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/pushbutton.cpp000066400000000000000000000064231503074453300240330ustar00rootroot00000000000000#include "pushbutton.hpp" #include #include #include #include #include #include "../../model/prefs/shortcutmanager.hpp" #include "../../model/prefs/state.hpp" void CSVWidget::PushButton::processShortcuts() { mProcessedToolTip = CSMPrefs::State::get().getShortcutManager().processToolTip(mToolTip); } void CSVWidget::PushButton::setExtendedToolTip() { QString tooltip = mProcessedToolTip; if (tooltip.isEmpty()) tooltip = "(Tool tip not implemented yet)"; switch (mType) { case Type_TopMode: tooltip += "

(left click to change mode)"; break; case Type_TopAction: break; case Type_Mode: tooltip += "

(left click to activate," "
shift-left click to activate and keep panel open)"; break; case Type_Toggle: tooltip += "

(left click to "; tooltip += isChecked() ? "disable" : "enable"; tooltip += "

shift-left click to "; tooltip += isChecked() ? "disable" : "enable"; tooltip += " and keep panel open)"; break; } setToolTip(tooltip); } void CSVWidget::PushButton::keyPressEvent(QKeyEvent* event) { if (event->key() != Qt::Key_Shift) mKeepOpen = false; QPushButton::keyPressEvent(event); } void CSVWidget::PushButton::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Space) mKeepOpen = event->modifiers() & Qt::ShiftModifier; QPushButton::keyReleaseEvent(event); } void CSVWidget::PushButton::mouseReleaseEvent(QMouseEvent* 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) { if (type == Type_Mode || type == Type_Toggle) { setCheckable(true); connect(this, &PushButton::toggled, this, &PushButton::checkedStateChanged); } setCheckable(type == Type_Mode || type == Type_Toggle); processShortcuts(); setExtendedToolTip(); 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) { setCheckable(type == Type_Mode || type == Type_Toggle); processShortcuts(); setExtendedToolTip(); } bool CSVWidget::PushButton::hasKeepOpen() const { return mKeepOpen; } QString CSVWidget::PushButton::getBaseToolTip() const { return mProcessedToolTip; } CSVWidget::PushButton::Type CSVWidget::PushButton::getType() const { return mType; } void CSVWidget::PushButton::checkedStateChanged(bool checked) { setExtendedToolTip(); } void CSVWidget::PushButton::settingChanged(const CSMPrefs::Setting* setting) { if (setting->getParent()->getKey() == "Key Bindings") { processShortcuts(); setExtendedToolTip(); } } openmw-openmw-0.49.0/apps/opencs/view/widget/pushbutton.hpp000066400000000000000000000030771503074453300240420ustar00rootroot00000000000000#ifndef CSV_WIDGET_PUSHBUTTON_H #define CSV_WIDGET_PUSHBUTTON_H #include class QKeyEvent; class QMouseEvent; class QObject; class QWidget; namespace CSMPrefs { class Setting; } 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: void processShortcuts(); void setExtendedToolTip(); protected: void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; 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); bool hasKeepOpen() const; /// Return tooltip used at construction (without any button-specific modifications) QString getBaseToolTip() const; Type getType() const; private slots: void checkedStateChanged(bool checked); void settingChanged(const CSMPrefs::Setting* setting); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetool.cpp000066400000000000000000000017241503074453300236120ustar00rootroot00000000000000#include "scenetool.hpp" #include #include #include "scenetoolbar.hpp" 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()); connect(this, &SceneTool::clicked, this, &SceneTool::openRequest); } void CSVWidget::SceneTool::activate() {} void CSVWidget::SceneTool::mouseReleaseEvent(QMouseEvent* event) { if (getType() == Type_TopAction && event->button() == Qt::RightButton) showPanel(parentWidget()->mapToGlobal(pos())); else PushButton::mouseReleaseEvent(event); } void CSVWidget::SceneTool::openRequest() { if (getType() == Type_TopAction) activate(); else showPanel(parentWidget()->mapToGlobal(pos())); } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetool.hpp000066400000000000000000000013271503074453300236160ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOL_H #define CSV_WIDGET_SCENETOOL_H #include "pushbutton.hpp" class QMouseEvent; class QObject; class QPoint; namespace CSVWidget { class SceneToolbar; ///< \brief Tool base class class SceneTool : public PushButton { Q_OBJECT public: SceneTool(SceneToolbar* parent, Type type = Type_TopMode); virtual void showPanel(const QPoint& position) = 0; /// This function will only called for buttons of type Type_TopAction. The default /// implementation is empty. virtual void activate(); protected: void mouseReleaseEvent(QMouseEvent* event) override; private slots: void openRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolbar.cpp000066400000000000000000000026661503074453300243050ustar00rootroot00000000000000#include "scenetoolbar.hpp" #include #include #include "../../model/prefs/shortcut.hpp" #include "scenetool.hpp" void CSVWidget::SceneToolbar::focusInEvent(QFocusEvent* event) { QWidget::focusInEvent(event); if (mLayout->count()) dynamic_cast(*mLayout->itemAt(0)).widget()->setFocus(); } CSVWidget::SceneToolbar::SceneToolbar(int buttonSize, QWidget* parent) : QWidget(parent) , mButtonSize(buttonSize) , mIconSize(buttonSize - 6) { setFixedWidth(mButtonSize); mLayout = new QVBoxLayout(this); mLayout->setAlignment(Qt::AlignTop); mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); setLayout(mLayout); CSMPrefs::Shortcut* focusSceneShortcut = new CSMPrefs::Shortcut("scene-focus-toolbar", this); connect(focusSceneShortcut, qOverload<>(&CSMPrefs::Shortcut::activated), this, &SceneToolbar::focusSceneRequest); } void CSVWidget::SceneToolbar::addTool(SceneTool* tool, SceneTool* insertPoint) { if (!insertPoint) mLayout->addWidget(tool, 0, Qt::AlignTop); else { int index = mLayout->indexOf(insertPoint); mLayout->insertWidget(index + 1, tool, 0, Qt::AlignTop); } } void CSVWidget::SceneToolbar::removeTool(SceneTool* tool) { mLayout->removeWidget(tool); } int CSVWidget::SceneToolbar::getButtonSize() const { return mButtonSize; } int CSVWidget::SceneToolbar::getIconSize() const { return mIconSize; } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolbar.hpp000066400000000000000000000014751503074453300243070ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOLBAR_H #define CSV_WIDGET_SCENETOOLBAR_H #include class QVBoxLayout; namespace CSVWidget { class SceneTool; class SceneToolbar : public QWidget { Q_OBJECT QVBoxLayout* mLayout; int mButtonSize; int mIconSize; protected: void focusInEvent(QFocusEvent* event) override; public: SceneToolbar(int buttonSize, QWidget* parent = nullptr); /// 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); void removeTool(SceneTool* tool); int getButtonSize() const; int getIconSize() const; signals: void focusSceneRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolmode.cpp000066400000000000000000000102231503074453300244510ustar00rootroot00000000000000#include "scenetoolmode.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "modebutton.hpp" #include "scenetoolbar.hpp" void CSVWidget::SceneToolMode::contextMenuEvent(QContextMenuEvent* event) { QMenu menu(this); if (createContextMenu(&menu)) menu.exec(event->globalPos()); } bool CSVWidget::SceneToolMode::createContextMenu(QMenu* menu) { if (mCurrent) return mCurrent->createContextMenu(menu); return false; } void CSVWidget::SceneToolMode::adjustToolTip(const ModeButton* activeMode) { QString toolTip = mToolTip; toolTip += "

Currently selected: " + activeMode->getBaseToolTip(); toolTip += "

(left click to change mode)"; if (createContextMenu(nullptr)) toolTip += "
(right click to access context menu)"; setToolTip(toolTip); } 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); setIcon(iter->first->icon()); adjustToolTip(iter->first); if (mCurrent != iter->first) { if (mCurrent) mCurrent->deactivate(mToolbar); mCurrent = iter->first; mCurrent->activate(mToolbar); } 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) { mPanel = new QFrame(this, Qt::Popup); mLayout = new QHBoxLayout(mPanel); mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); mPanel->setLayout(mLayout); } void CSVWidget::SceneToolMode::showPanel(const QPoint& position) { mPanel->move(position); mPanel->show(); if (mFirst) mFirst->setFocus(Qt::OtherFocusReason); } void CSVWidget::SceneToolMode::addButton(const std::string& icon, const std::string& id, const QString& tooltip) { 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) { button->setParent(mPanel); button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize(QSize(mIconSize, mIconSize)); button->setFixedSize(mButtonSize, mButtonSize); mLayout->addWidget(button); mButtons.insert(std::make_pair(button, id)); connect(button, &ModeButton::clicked, this, &SceneToolMode::selected); if (mButtons.size() == 1) { mFirst = mCurrent = button; setIcon(button->icon()); button->setChecked(true); adjustToolTip(button); mCurrent->activate(mToolbar); } } CSVWidget::ModeButton* CSVWidget::SceneToolMode::getCurrent() { return mCurrent; } std::string CSVWidget::SceneToolMode::getCurrentId() const { auto currentButton = mButtons.find(mCurrent); assert(currentButton != mButtons.end()); return currentButton->second; } void CSVWidget::SceneToolMode::setButton(const std::string& id) { for (std::map::iterator iter = mButtons.begin(); iter != mButtons.end(); ++iter) if (iter->second == id) { setButton(iter); break; } } bool CSVWidget::SceneToolMode::event(QEvent* event) { if (event->type() == QEvent::ToolTip) { adjustToolTip(mCurrent); } return SceneTool::event(event); } void CSVWidget::SceneToolMode::selected() { std::map::iterator iter = mButtons.find(dynamic_cast(sender())); if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); setButton(iter); } } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolmode.hpp000066400000000000000000000043321503074453300244620ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOL_MODE_H #define CSV_WIDGET_SCENETOOL_MODE_H #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 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 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); void setButton(std::map::iterator iter); protected: bool event(QEvent* event) override; public: SceneToolMode(SceneToolbar* parent, const QString& toolTip); void showPanel(const QPoint& position) override; void addButton(const std::string& icon, const std::string& id, const QString& tooltip = ""); /// The ownership of \a button is transferred to *this. void addButton(ModeButton* button, const std::string& id); /// Will return a 0-pointer only if the mode does not have any buttons yet. ModeButton* getCurrent(); /// Must not be called if there aren't any buttons yet. std::string getCurrentId() const; /// Manually change the current mode void setButton(const std::string& id); signals: void modeChanged(const std::string& id); private slots: void selected(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolrun.cpp000066400000000000000000000076521503074453300243450ustar00rootroot00000000000000#include "scenetoolrun.hpp" #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()) toolTip += "

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

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

(right click to switch to a different profile)"; } setToolTip(toolTip); } void CSVWidget::SceneToolRun::updateIcon() { setDisabled(mSelected == mProfiles.end()); } void CSVWidget::SceneToolRun::updatePanel() { mTable->setRowCount(static_cast(mProfiles.size())); int i = 0; 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, 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) { setIcon(Misc::ScalableIcon::load(icon)); updateIcon(); adjustToolTips(); mPanel = new QFrame(this, Qt::Popup); QHBoxLayout* layout = new QHBoxLayout(mPanel); layout->setContentsMargins(QMargins(0, 0, 0, 0)); mTable = new QTableWidget(0, 2, this); 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); layout->addWidget(mTable); connect(mTable, &QTableWidget::clicked, this, &SceneToolRun::clicked); } void CSVWidget::SceneToolRun::showPanel(const QPoint& position) { updatePanel(); mPanel->move(position); mPanel->show(); } void CSVWidget::SceneToolRun::activate() { if (mSelected != mProfiles.end()) emit runRequest(*mSelected); } void CSVWidget::SceneToolRun::removeProfile(const std::string& profile) { std::set::iterator iter = mProfiles.find(profile); if (iter != mProfiles.end()) { if (iter == mSelected) { if (iter != mProfiles.begin()) --mSelected; else ++mSelected; } mProfiles.erase(iter); if (mSelected == mProfiles.end()) updateIcon(); adjustToolTips(); } } void CSVWidget::SceneToolRun::addProfile(const std::string& profile) { std::set::iterator iter = mProfiles.find(profile); if (iter == mProfiles.end()) { mProfiles.insert(profile); if (mSelected == mProfiles.end()) { mSelected = mProfiles.begin(); updateIcon(); } adjustToolTips(); } } void CSVWidget::SceneToolRun::clicked(const QModelIndex& index) { if (index.column() == 0) { // select profile mSelected = mProfiles.begin(); std::advance(mSelected, index.row()); mPanel->hide(); adjustToolTips(); } else if (index.column() == 1) { // remove profile from list std::set::iterator iter = mProfiles.begin(); std::advance(iter, index.row()); removeProfile(*iter); updatePanel(); } } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolrun.hpp000066400000000000000000000027041503074453300243430ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOLRUN_H #define CSV_WIDGET_SCENETOOLRUN_H #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: void adjustToolTips(); void updateIcon(); void updatePanel(); public: SceneToolRun(SceneToolbar* parent, const QString& toolTip, const QString& icon, const std::vector& profiles); void showPanel(const QPoint& position) 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 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: void clicked(const QModelIndex& index); signals: void runRequest(const std::string& profile); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolshapebrush.cpp000066400000000000000000000213271503074453300257000ustar00rootroot00000000000000#include "scenetoolshapebrush.hpp" #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/prefs/state.hpp" namespace CSVWidget { class SceneToolbar; } namespace CSMDoc { class Document; } CSVWidget::ShapeBrushSizeControls::ShapeBrushSizeControls(const QString& title, QWidget* parent) : QGroupBox(title, parent) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); mBrushSizeSlider->setTickInterval(10); mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mBrushSizeSlider->setSingleStep(1); mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["shapebrush-maximumsize"].toInt()); mBrushSizeSpinBox->setSingleStep(1); QHBoxLayout* layoutSliderSize = new QHBoxLayout; layoutSliderSize->addWidget(mBrushSizeSlider); layoutSliderSize->addWidget(mBrushSizeSpinBox); 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) { 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; layoutMain->setSpacing(0); layoutMain->setContentsMargins(4, 0, 4, 4); QHBoxLayout* layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(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); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); brushButtonGroup->addButton(mButtonSquare); brushButtonGroup->addButton(mButtonCircle); brushButtonGroup->addButton(mButtonCustom); brushButtonGroup->setExclusive(true); layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop); mHorizontalGroupBox = new QGroupBox(tr("")); mHorizontalGroupBox->setLayout(layoutHorizontal); mToolSelector = new QComboBox(this); mToolSelector->addItem(tr("Height (drag)")); mToolSelector->addItem(tr("Height, raise (paint)")); 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); brushStrengthLabel->setText("Brush strength:"); mToolStrengthSlider = new QSlider(Qt::Horizontal); mToolStrengthSlider->setTickPosition(QSlider::TicksBothSides); mToolStrengthSlider->setTickInterval(8); mToolStrengthSlider->setRange(8, 128); mToolStrengthSlider->setSingleStep(8); mToolStrengthSlider->setValue(8); layoutMain->addWidget(mHorizontalGroupBox); layoutMain->addWidget(mSizeSliders); layoutMain->addWidget(mToolSelector); layoutMain->addWidget(brushStrengthLabel); layoutMain->addWidget(mToolStrengthSlider); setLayout(layoutMain); 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) { 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) { mBrushSize = brushSize; emit passBrushSize(mBrushSize); } 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; emit passBrushShape(mBrushShape); } 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)) { setAcceptDrops(true); connect(mShapeBrushWindow, &ShapeBrushWindow::passBrushShape, this, &SceneToolShapeBrush::setButtonIcon); setButtonIcon(mShapeBrushWindow->mBrushShape); mPanel = new QFrame(this, Qt::Popup); QHBoxLayout* layout = new QHBoxLayout(mPanel); layout->setContentsMargins(QMargins(0, 0, 0, 0)); mTable = new QTableWidget(0, 2, this); 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); connect(mTable, &QTableWidget::clicked, this, &SceneToolShapeBrush::clicked); } void CSVWidget::SceneToolShapeBrush::setButtonIcon(CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

Currently selected: "; switch (brushShape) { case BrushShape_Point: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mShapeBrushWindow->toolTipPoint; break; case BrushShape_Square: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mShapeBrushWindow->toolTipSquare; break; case BrushShape_Circle: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mShapeBrushWindow->toolTipCircle; break; case BrushShape_Custom: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-custom")); tooltip += mShapeBrushWindow->toolTipCustom; break; } setToolTip(tooltip); } void CSVWidget::SceneToolShapeBrush::showPanel(const QPoint& position) {} void CSVWidget::SceneToolShapeBrush::updatePanel() {} void CSVWidget::SceneToolShapeBrush::clicked(const QModelIndex& index) {} 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->show(); } void CSVWidget::SceneToolShapeBrush::dragEnterEvent(QDragEnterEvent* event) { emit passEvent(event); event->accept(); } void CSVWidget::SceneToolShapeBrush::dropEvent(QDropEvent* event) { emit passEvent(event); event->accept(); } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetoolshapebrush.hpp000066400000000000000000000062461503074453300257100ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #define CSV_WIDGET_SCENETOOLSHAPEBRUSH_H #include #include #include #include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.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 { class TerrainShapeMode; } 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); private: QSlider* mBrushSizeSlider = new QSlider(Qt::Horizontal); QSpinBox* mBrushSizeSpinBox = new QSpinBox; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; }; /// \brief Brush settings window class ShapeBrushWindow : public QFrame { 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; friend class SceneToolShapeBrush; friend class CSVRender::TerrainShapeMode; public slots: void setBrushShape(); void setBrushSize(int brushSize); 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: void adjustToolTips(); public: SceneToolShapeBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document); void showPanel(const QPoint& position) override; void updatePanel(); 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; signals: void passEvent(QDropEvent* event); void passEvent(QDragEnterEvent* event); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltexturebrush.cpp000066400000000000000000000324551503074453300263040ustar00rootroot00000000000000#include "scenetooltexturebrush.hpp" #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/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) { mBrushSizeSlider->setTickPosition(QSlider::TicksBothSides); mBrushSizeSlider->setTickInterval(10); mBrushSizeSlider->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mBrushSizeSlider->setSingleStep(1); mBrushSizeSpinBox->setRange(1, CSMPrefs::get()["3D Scene Editing"]["texturebrush-maximumsize"].toInt()); mBrushSizeSpinBox->setSingleStep(1); mLayoutSliderSize->addWidget(mBrushSizeSlider); mLayoutSliderSize->addWidget(mBrushSizeSpinBox); 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) { mBrushTextureLabel = "Selected texture: " + mBrushTexture.getRefIdString() + " "; CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); 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 { mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush = new QLabel(QString::fromStdString(mBrushTextureLabel)); } 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; layoutMain->setSpacing(0); layoutMain->setContentsMargins(4, 0, 4, 4); QHBoxLayout* layoutHorizontal = new QHBoxLayout; layoutHorizontal->setSpacing(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); QButtonGroup* brushButtonGroup = new QButtonGroup(this); brushButtonGroup->addButton(mButtonPoint); brushButtonGroup->addButton(mButtonSquare); brushButtonGroup->addButton(mButtonCircle); brushButtonGroup->addButton(mButtonCustom); brushButtonGroup->setExclusive(true); layoutHorizontal->addWidget(mButtonPoint, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonSquare, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCircle, 0, Qt::AlignTop); layoutHorizontal->addWidget(mButtonCustom, 0, Qt::AlignTop); mHorizontalGroupBox = new QGroupBox(tr("")); mHorizontalGroupBox->setLayout(layoutHorizontal); layoutMain->addWidget(mHorizontalGroupBox); layoutMain->addWidget(mSizeSliders); layoutMain->addWidget(mSelectedBrush); setLayout(layoutMain); 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) { 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(ESM::RefId brushTexture) { CSMWorld::IdTable& ltexTable = dynamic_cast( *mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_LandTextures)); QUndoStack& undoStack = mDocument.getUndoStack(); CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); int row = landtexturesCollection.getIndex(brushTexture); const auto& record = landtexturesCollection.getRecord(row); if (!record.isDeleted()) { // Ensure the texture is defined by the current plugin if (!record.isModified()) { CSMWorld::CommandMacro macro(undoStack); macro.push(new CSMWorld::TouchCommand(ltexTable, brushTexture.getRefIdString())); } mBrushTextureLabel = "Selected texture: " + brushTexture.getRefIdString() + " "; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel) + landtexturesCollection.getData(row, landTextureFilename).value()); } else { brushTexture = {}; mBrushTextureLabel = "No selected texture or invalid texture"; mSelectedBrush->setText(QString::fromStdString(mBrushTextureLabel)); } mBrushTexture = brushTexture; emit passTextureId(mBrushTexture); emit passBrushShape(mBrushShape); // updates the icon tooltip } void CSVWidget::TextureBrushWindow::setBrushSize(int brushSize) { mBrushSize = brushSize; emit passBrushSize(mBrushSize); } void CSVWidget::TextureBrushWindow::setBrushShape() { if (mButtonPoint->isChecked()) mBrushShape = CSVWidget::BrushShape_Point; if (mButtonSquare->isChecked()) mBrushShape = CSVWidget::BrushShape_Square; if (mButtonCircle->isChecked()) mBrushShape = CSVWidget::BrushShape_Circle; if (mButtonCustom->isChecked()) mBrushShape = CSVWidget::BrushShape_Custom; emit passBrushShape(mBrushShape); } 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)) { mBrushHistory.resize(1); setAcceptDrops(true); connect(mTextureBrushWindow, &TextureBrushWindow::passBrushShape, this, &SceneToolTextureBrush::setButtonIcon); setButtonIcon(mTextureBrushWindow->mBrushShape); mPanel = new QFrame(this, Qt::Popup); QHBoxLayout* layout = new QHBoxLayout(mPanel); layout->setContentsMargins(QMargins(0, 0, 0, 0)); mTable = new QTableWidget(0, 2, this); 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); connect(mTable, &QTableWidget::clicked, this, &SceneToolTextureBrush::clicked); } void CSVWidget::SceneToolTextureBrush::setButtonIcon(CSVWidget::BrushShape brushShape) { QString tooltip = "Change brush settings

Currently selected: "; switch (brushShape) { case BrushShape_Point: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-point")); tooltip += mTextureBrushWindow->toolTipPoint; break; case BrushShape_Square: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-square")); tooltip += mTextureBrushWindow->toolTipSquare; break; case BrushShape_Circle: setIcon(Misc::ScalableIcon::load(":scenetoolbar/brush-circle")); tooltip += mTextureBrushWindow->toolTipCircle; break; case BrushShape_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(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); const int index = landtexturesCollection.searchId(mTextureBrushWindow->mBrushTexture); if (index != -1 && !landtexturesCollection.getRecord(index).isDeleted()) { tooltip += "

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

No selected texture or invalid texture"; } tooltip += "
(drop texture here to change)"; setToolTip(tooltip); } void CSVWidget::SceneToolTextureBrush::showPanel(const QPoint& position) { updatePanel(); mPanel->move(position); mPanel->show(); } void CSVWidget::SceneToolTextureBrush::updatePanel() { mTable->setRowCount(mBrushHistory.size()); for (int i = mBrushHistory.size() - 1; i >= 0; --i) { CSMWorld::IdCollection& landtexturesCollection = mDocument.getData().getLandTextures(); int landTextureFilename = landtexturesCollection.findColumnIndex(CSMWorld::Columns::ColumnId_Texture); 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].getRefIdString()))); } else { mTable->setItem(i, 1, new QTableWidgetItem("Invalid/deleted texture")); mTable->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(mBrushHistory[i].getRefIdString()))); } } } void CSVWidget::SceneToolTextureBrush::updateBrushHistory(ESM::RefId brushTexture) { mBrushHistory.insert(mBrushHistory.begin(), brushTexture); if (mBrushHistory.size() > 5) mBrushHistory.pop_back(); } void CSVWidget::SceneToolTextureBrush::clicked(const QModelIndex& index) { if (index.column() == 0 || index.column() == 1) { ESM::RefId brushTexture = mBrushHistory[index.row()]; std::swap(mBrushHistory[index.row()], mBrushHistory[0]); mTextureBrushWindow->setBrushTexture(brushTexture); emit passTextureId(brushTexture); updatePanel(); mPanel->hide(); } } 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->show(); } void CSVWidget::SceneToolTextureBrush::dragEnterEvent(QDragEnterEvent* event) { emit passEvent(event); event->accept(); } void CSVWidget::SceneToolTextureBrush::dropEvent(QDropEvent* event) { emit passEvent(event); event->accept(); } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltexturebrush.hpp000066400000000000000000000067761503074453300263200ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #define CSV_WIDGET_SCENETOOLTEXTUREBRUSH_H #include #include #ifndef Q_MOC_RUN #include "brushshapes.hpp" #include "scenetool.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 SceneToolbar; /// \brief Layout-box for some brush button settings class BrushSizeControls : public QGroupBox { Q_OBJECT public: BrushSizeControls(const QString& title, QWidget* parent); private: QHBoxLayout* mLayoutSliderSize; QSlider* mBrushSizeSlider; QSpinBox* mBrushSizeSpinBox; friend class SceneToolTextureBrush; friend class CSVRender::TerrainTextureMode; }; /// \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; 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(ESM::RefId brushTexture); void setBrushShape(); void setBrushSize(int brushSize); 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: void adjustToolTips(); public: SceneToolTextureBrush(SceneToolbar* parent, const QString& toolTip, CSMDoc::Document& document); void showPanel(const QPoint& position) override; void updatePanel(); void dropEvent(QDropEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override; friend class CSVRender::TerrainTextureMode; 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(ESM::RefId brushTexture); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltoggle.cpp000066400000000000000000000125741503074453300250210ustar00rootroot00000000000000#include "scenetooltoggle.hpp" #include #include #include #include #include #include #include #include #include "pushbutton.hpp" #include "scenetoolbar.hpp" void CSVWidget::SceneToolToggle::adjustToolTip() { QString toolTip = mToolTip; toolTip += "

Currently enabled: "; bool first = true; for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) toolTip += ", "; else first = false; toolTip += iter->second.mName; } if (first) toolTip += "none"; toolTip += "

(left click to alter selection)"; setToolTip(toolTip); } void CSVWidget::SceneToolToggle::adjustIcon() { unsigned int selection = getSelectionMask(); if (!selection) setIcon(QIcon(QString::fromUtf8(mEmptyIcon.c_str()))); else { QPixmap pixmap(48, 48); pixmap.fill(QColor(0, 0, 0, 0)); { QPainter painter(&pixmap); 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()))); } } setIcon(pixmap); } } QRect CSVWidget::SceneToolToggle::getIconBox(int index) const { // layout for a 3x3 grid int xMax = 3; int yMax = 3; // icon size int xBorder = 1; int yBorder = 1; 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; if (total % xMax) ++actualYIcons; 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); } if (y == actualYIcons - 1) { // generating the last row of icons int actualXIcons = total % xMax; if (actualXIcons) { // space out icons horizontally, if there aren't enough to fill the last row int diff = xMax - actualXIcons; xBorder += (diff * (xBorder + iconXSize)) / (actualXIcons + 1); } } 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) { mPanel = new QFrame(this, Qt::Popup); mLayout = new QHBoxLayout(mPanel); mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); mPanel->setLayout(mLayout); } void CSVWidget::SceneToolToggle::showPanel(const QPoint& position) { mPanel->move(position); mPanel->show(); if (mFirst) 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) { 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); button->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)); button->setIconSize(QSize(mIconSize, mIconSize)); button->setFixedSize(mButtonSize, mButtonSize); mLayout->addWidget(button); ButtonDesc desc; desc.mMask = mask; desc.mSmallIcon = smallIcon; desc.mName = name; desc.mIndex = static_cast(mButtons.size()); mButtons.insert(std::make_pair(button, desc)); connect(button, &PushButton::clicked, this, &SceneToolToggle::selected); if (mButtons.size() == 1) mFirst = button; } unsigned int CSVWidget::SceneToolToggle::getSelectionMask() const { unsigned int selection = 0; 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) { for (std::map::iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) iter->first->setChecked(selection & iter->second.mMask); adjustToolTip(); adjustIcon(); } void CSVWidget::SceneToolToggle::selected() { std::map::const_iterator iter = mButtons.find(dynamic_cast(sender())); if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); adjustToolTip(); adjustIcon(); emit selectionChanged(); } } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltoggle.hpp000066400000000000000000000036321503074453300250210ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOL_TOGGLE_H #define CSV_WIDGET_SCENETOOL_TOGGLE_H #include "scenetool.hpp" #include #include class QHBoxLayout; class QRect; class QObject; class QPoint; class QWidget; namespace CSVWidget { class SceneToolbar; class PushButton; ///< \brief Multi-Toggle tool class SceneToolToggle : public SceneTool { Q_OBJECT 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; void adjustToolTip(); void adjustIcon(); QRect getIconBox(int index) const; public: SceneToolToggle(SceneToolbar* parent, const QString& toolTip, const std::string& emptyIcon); 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 = ""); unsigned int getSelectionMask() const; /// \param or'ed button masks. buttons that do not exist will be ignored. void setSelectionMask(unsigned int selection); signals: void selectionChanged(); private slots: void selected(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltoggle2.cpp000066400000000000000000000077431503074453300251050ustar00rootroot00000000000000#include "scenetooltoggle2.hpp" #include #include #include #include #include #include #include #include #include "pushbutton.hpp" #include "scenetoolbar.hpp" void CSVWidget::SceneToolToggle2::adjustToolTip() { QString toolTip = mToolTip; toolTip += "

Currently enabled: "; bool first = true; for (std::map::const_iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) if (iter->first->isChecked()) { if (!first) toolTip += ", "; else first = false; toolTip += iter->second.mName; } if (first) toolTip += "none"; toolTip += "

(left click to alter selection)"; setToolTip(toolTip); } void CSVWidget::SceneToolToggle2::adjustIcon() { unsigned int buttonIds = 0; 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(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) { mPanel = new QFrame(this, Qt::Popup); mLayout = new QHBoxLayout(mPanel); mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); mPanel->setLayout(mLayout); } void CSVWidget::SceneToolToggle2::showPanel(const QPoint& position) { mPanel->move(position); mPanel->show(); if (mFirst) mFirst->setFocus(Qt::OtherFocusReason); } 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(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); if (disabled) button->setDisabled(true); mLayout->addWidget(button); ButtonDesc desc; desc.mButtonId = id; desc.mMask = mask; desc.mName = name; desc.mIndex = static_cast(mButtons.size()); mButtons.insert(std::make_pair(button, desc)); connect(button, &QPushButton::clicked, this, &SceneToolToggle2::selected); if (mButtons.size() == 1 && !disabled) mFirst = button; } unsigned int CSVWidget::SceneToolToggle2::getSelectionMask() const { unsigned int selection = 0; 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) { for (std::map::iterator iter(mButtons.begin()); iter != mButtons.end(); ++iter) iter->first->setChecked(selection & iter->second.mMask); adjustToolTip(); adjustIcon(); } void CSVWidget::SceneToolToggle2::selected() { std::map::const_iterator iter = mButtons.find(dynamic_cast(sender())); if (iter != mButtons.end()) { if (!iter->first->hasKeepOpen()) mPanel->hide(); adjustToolTip(); adjustIcon(); emit selectionChanged(); } } openmw-openmw-0.49.0/apps/opencs/view/widget/scenetooltoggle2.hpp000066400000000000000000000042041503074453300250770ustar00rootroot00000000000000#ifndef CSV_WIDGET_SCENETOOL_TOGGLE2_H #define CSV_WIDGET_SCENETOOL_TOGGLE2_H #include "scenetool.hpp" #include #include class QHBoxLayout; class QObject; class QPoint; class QWidget; namespace CSVWidget { class SceneToolbar; class PushButton; ///< \brief Multi-Toggle tool /// /// Top level button is using predefined icons instead building a composite icon. class SceneToolToggle2 : public SceneTool { Q_OBJECT 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; void adjustToolTip(); void adjustIcon(); 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); 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); unsigned int getSelectionMask() const; /// \param or'ed button masks. buttons that do not exist will be ignored. void setSelectionMask(unsigned int selection); signals: void selectionChanged(); private slots: void selected(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/000077500000000000000000000000001503074453300207535ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs/view/world/bodypartcreator.cpp000066400000000000000000000022101503074453300246560ustar00rootroot00000000000000#include "bodypartcreator.hpp" #include #include "../../model/world/data.hpp" #include "../../model/world/universalid.hpp" #include std::string CSVWorld::BodyPartCreator::getId() const { std::string id = CSVWorld::GenericCreator::getId(); if (mFirstPerson->isChecked()) { id += ".1st"; } return 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, &QCheckBox::clicked, this, &BodyPartCreator::checkboxClicked); } std::string CSVWorld::BodyPartCreator::getErrors() const { std::string errors; std::string id = getId(); if (getData().hasId(id)) { errors = "ID is already in use"; } return errors; } void CSVWorld::BodyPartCreator::reset() { CSVWorld::GenericCreator::reset(); mFirstPerson->setChecked(false); } void CSVWorld::BodyPartCreator::checkboxClicked() { update(); } openmw-openmw-0.49.0/apps/opencs/view/world/bodypartcreator.hpp000066400000000000000000000015211503074453300246670ustar00rootroot00000000000000#ifndef BODYPARTCREATOR_HPP #define BODYPARTCREATOR_HPP class QCheckBox; #include "genericcreator.hpp" namespace CSMWorld { class Data; class UniversalId; } namespace CSVWorld { /// \brief Record creator for body parts. class BodyPartCreator : public GenericCreator { Q_OBJECT QCheckBox* mFirstPerson; private: /// \return ID entered by user. std::string getId() const override; public: BodyPartCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); /// \return Error description for current user input. std::string getErrors() const override; /// \brief Clear ID and checkbox input widgets. void reset() override; private slots: void checkboxClicked(); }; } #endif // BODYPARTCREATOR_HPP openmw-openmw-0.49.0/apps/opencs/view/world/cellcreator.cpp000066400000000000000000000072441503074453300237650ustar00rootroot00000000000000#include "cellcreator.hpp" #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) return GenericCreator::getId(); std::ostringstream stream; stream << "#" << mX->value() << " " << mY->value(); return stream.str(); } void CSVWorld::CellCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { CSMWorld::IdTree* model = &dynamic_cast(*getData().getTableModel(getCollectionId())); int parentIndex = model->findColumnIndex(CSMWorld::Columns::ColumnId_Cell); int index = model->findNestedColumnIndex(parentIndex, CSMWorld::Columns::ColumnId_Interior); command.addNestedValue(parentIndex, index, mType->currentIndex() == 0); } 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, qOverload(&QSpinBox::valueChanged), this, &CellCreator::valueChanged); insertAtBeginning(mY, true); 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, qOverload(&QSpinBox::valueChanged), this, &CellCreator::valueChanged); insertAtBeginning(mX, true); mXLabel = new QLabel("X", this); mXLabel->setVisible(false); insertAtBeginning(mXLabel, false); mType = new QComboBox(this); mType->addItem("Interior Cell"); mType->addItem("Exterior Cell"); connect(mType, qOverload(&QComboBox::currentIndexChanged), this, &CellCreator::setType); insertAtBeginning(mType, false); } void CSVWorld::CellCreator::reset() { mX->setValue(0); mY->setValue(0); mType->setCurrentIndex(0); setType(0); GenericCreator::reset(); } 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); // 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) { update(); } 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 { setType(1); // enable x and y controls mType->setCurrentIndex(1); } else { setType(0); mType->setCurrentIndex(0); } } std::string CSVWorld::CellCreator::getErrors() const { std::string errors; if (mType->currentIndex() == 0) { errors = GenericCreator::getErrors(); } else if (getData().hasId(getId())) { errors = "The Exterior Cell is already exist"; } return errors; } openmw-openmw-0.49.0/apps/opencs/view/world/cellcreator.hpp000066400000000000000000000024161503074453300237660ustar00rootroot00000000000000#ifndef CSV_WORLD_CELLCREATOR_H #define CSV_WORLD_CELLCREATOR_H #include #include #include "genericcreator.hpp" class QComboBox; class QLabel; class QObject; class QSpinBox; class QUndoStack; 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: std::string getId() const override; /// Allow subclasses to add additional data to \a command. void configureCreateCommand(CSMWorld::CreateCommand& command) const override; public: CellCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void reset() 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. private slots: void setType(int index); void valueChanged(int index); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/colordelegate.cpp000066400000000000000000000023701503074453300242720ustar00rootroot00000000000000#include "colordelegate.hpp" #include #include #include #include #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 { 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); painter->save(); painter->fillRect(coloredRect, color); painter->setPen(Qt::black); painter->drawRect(coloredRect); painter->restore(); } CSVWorld::CommandDelegate* CSVWorld::ColorDelegateFactory::makeDelegate( CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new ColorDelegate(dispatcher, document, parent); } openmw-openmw-0.49.0/apps/opencs/view/world/colordelegate.hpp000066400000000000000000000016061503074453300243000ustar00rootroot00000000000000#ifndef CSV_WORLD_COLORDELEGATE_HPP #define CSV_WORLD_COLORDELEGATE_HPP #include "util.hpp" class QModelIndex; class QObject; class QPainter; namespace CSMDoc { 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; }; 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/creator.cpp000066400000000000000000000007661503074453300231270ustar00rootroot00000000000000#include "creator.hpp" #include #include #include namespace CSMDoc { class Document; } void CSVWorld::Creator::setScope(unsigned int scope) { if (scope != CSMWorld::Scope_Content) throw std::logic_error("Invalid scope in creator"); } CSVWorld::Creator* CSVWorld::NullCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return nullptr; } openmw-openmw-0.49.0/apps/opencs/view/world/creator.hpp000066400000000000000000000056731503074453300231360ustar00rootroot00000000000000#ifndef CSV_WORLD_CREATOR_H #define CSV_WORLD_CREATOR_H #include #include #include #ifndef Q_MOC_RUN #include "../../model/doc/document.hpp" #include "../../model/world/scope.hpp" #include "../../model/world/universalid.hpp" #endif namespace CSVWorld { /// \brief Record creator UI base class class Creator : public QWidget { Q_OBJECT public: ~Creator() override = default; virtual void reset() = 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; virtual void setEditLock(bool locked) = 0; virtual void toggleWidgets(bool active = true) = 0; /// Default implementation: Throw an exception if scope!=Scope_Content. virtual void setScope(unsigned int scope); /// Focus main input widget virtual void focus() = 0; signals: 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. }; /// \brief Base class for Creator factory class CreatorFactoryBase { 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. }; 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. }; template Creator* CreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { auto creator = std::make_unique(document.getData(), document.getUndoStack(), id); creator->setScope(scope); return creator.release(); } } #endif openmw-openmw-0.49.0/apps/opencs/view/world/datadisplaydelegate.cpp000066400000000000000000000136121503074453300254540ustar00rootroot00000000000000#include "datadisplaydelegate.hpp" #include "../../model/prefs/state.hpp" #include #include #include #include #include #include #include #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()); } 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(); IconList::iterator it = mIcons.begin(); while (it != mIcons.end()) { mPixmaps.emplace_back(it->mValue, it->mIcon.pixmap(mIconSize)); ++it; } } void CSVWorld::DataDisplayDelegate::setIconSize(const QSize& size) { mIconSize = size; buildPixmaps(); } void CSVWorld::DataDisplayDelegate::setTextLeftOffset(int offset) { mTextLeftOffset = offset; } QSize CSVWorld::DataDisplayDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { QSize size = EnumDelegate::sizeHint(option, index); int valueIndex = getValueIndex(index); if (valueIndex != -1) { if (mDisplayMode == Mode_IconOnly) { size.setWidth(mIconSize.width() + 2 * mHorizontalMargin); } else if (mDisplayMode == Mode_IconAndText) { size.setWidth(size.width() + mIconSize.width() + mTextLeftOffset); } if (mDisplayMode != Mode_TextOnly) { size.setHeight(qMax(size.height(), mIconSize.height())); } } return size; } 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 if (mDisplayMode == Mode_TextOnly) EnumDelegate::paint(painter, option, index); else { int valueIndex = getValueIndex(index); if (valueIndex != -1) { paintIcon(painter, option, valueIndex); } } painter->restore(); } void CSVWorld::DataDisplayDelegate::paintIcon(QPainter* painter, const QStyleOptionViewItem& option, int index) const { QRect iconRect = option.rect; QRect textRect = iconRect; iconRect.setLeft(iconRect.left() + mHorizontalMargin); iconRect.setRight(option.rect.right() - mHorizontalMargin); if (mDisplayMode == Mode_IconAndText) { iconRect.setWidth(mIconSize.width()); 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); } QApplication::style()->drawItemPixmap(painter, iconRect, Qt::AlignCenter, mPixmaps.at(index).second); } void CSVWorld::DataDisplayDelegate::updateDisplayMode(const std::string& mode) { if (mode == "Icon and Text") mDisplayMode = Mode_IconAndText; else if (mode == "Icon Only") mDisplayMode = Mode_IconOnly; else if (mode == "Text Only") mDisplayMode = Mode_TextOnly; } 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) { EnumDelegateFactory::add(enumValue, enumName); Icon icon; icon.mValue = enumValue; icon.mName = enumName; icon.mIcon = Misc::ScalableIcon::load(iconFilename); for (auto it = mIcons.begin(); it != mIcons.end(); ++it) { if (it->mName > enumName) { mIcons.insert(it, icon); return; } } mIcons.push_back(icon); } CSVWorld::CommandDelegate* CSVWorld::DataDisplayDelegateFactory::makeDelegate( CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new DataDisplayDelegate(mValues, mIcons, dispatcher, document, "", "", parent); } openmw-openmw-0.49.0/apps/opencs/view/world/datadisplaydelegate.hpp000077500000000000000000000060151503074453300254630ustar00rootroot00000000000000#ifndef DATADISPLAYDELEGATE_HPP #define DATADISPLAYDELEGATE_HPP #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; } namespace CSVWorld { class CommandDelegate; struct Icon { int mValue; QIcon mIcon; QString mName; }; class DataDisplayDelegate : public EnumDelegate { Q_OBJECT public: typedef std::vector IconList; typedef std::vector> ValueList; protected: enum DisplayMode { Mode_TextOnly, Mode_IconOnly, Mode_IconAndText }; DisplayMode mDisplayMode; IconList mIcons; private: 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() = default; void paint(QPainter* painter, 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); /// offset the horizontal position of the text from the right edge of the icon. Default is 8 pixels. void setTextLeftOffset(int offset); bool eventFilter(QObject* target, QEvent* event) override; private: /// update the display mode based on a passed 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; /// rebuild the list of pixmaps from the provided icons (called when icon size is changed) void buildPixmaps(); 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; ///< The ownership of the returned CommandDelegate is transferred to the caller. protected: void add(int enumValue, const QString& enumName, const QString& iconFilename); }; } #endif // DATADISPLAYDELEGATE_HPP openmw-openmw-0.49.0/apps/opencs/view/world/dialoguecreator.cpp000066400000000000000000000025341503074453300246340ustar00rootroot00000000000000#include "dialoguecreator.hpp" #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" class QUndoStack; void CSVWorld::DialogueCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { int index = dynamic_cast(*getData().getTableModel(getCollectionId())) .findColumnIndex(CSMWorld::Columns::ColumnId_DialogueType); 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::Creator* CSVWorld::TopicCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new DialogueCreator(document.getData(), document.getUndoStack(), id, ESM::Dialogue::Topic); } CSVWorld::Creator* CSVWorld::JournalCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new DialogueCreator(document.getData(), document.getUndoStack(), id, ESM::Dialogue::Journal); } openmw-openmw-0.49.0/apps/opencs/view/world/dialoguecreator.hpp000066400000000000000000000022751503074453300246430ustar00rootroot00000000000000#ifndef CSV_WORLD_DIALOGUECREATOR_H #define CSV_WORLD_DIALOGUECREATOR_H #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: void configureCreateCommand(CSMWorld::CreateCommand& command) const override; 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. }; 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/dialoguespinbox.cpp000066400000000000000000000023411503074453300246530ustar00rootroot00000000000000#include "dialoguespinbox.hpp" #include CSVWorld::DialogueSpinBox::DialogueSpinBox(QWidget* parent) : QSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } void CSVWorld::DialogueSpinBox::focusInEvent(QFocusEvent* event) { setFocusPolicy(Qt::WheelFocus); QSpinBox::focusInEvent(event); } void CSVWorld::DialogueSpinBox::focusOutEvent(QFocusEvent* event) { setFocusPolicy(Qt::StrongFocus); QSpinBox::focusOutEvent(event); } void CSVWorld::DialogueSpinBox::wheelEvent(QWheelEvent* event) { if (!hasFocus()) event->ignore(); else QSpinBox::wheelEvent(event); } CSVWorld::DialogueDoubleSpinBox::DialogueDoubleSpinBox(QWidget* parent) : QDoubleSpinBox(parent) { setFocusPolicy(Qt::StrongFocus); } void CSVWorld::DialogueDoubleSpinBox::focusInEvent(QFocusEvent* event) { setFocusPolicy(Qt::WheelFocus); QDoubleSpinBox::focusInEvent(event); } void CSVWorld::DialogueDoubleSpinBox::focusOutEvent(QFocusEvent* event) { setFocusPolicy(Qt::StrongFocus); QDoubleSpinBox::focusOutEvent(event); } void CSVWorld::DialogueDoubleSpinBox::wheelEvent(QWheelEvent* event) { if (!hasFocus()) event->ignore(); else QDoubleSpinBox::wheelEvent(event); } openmw-openmw-0.49.0/apps/opencs/view/world/dialoguespinbox.hpp000066400000000000000000000015151503074453300246620ustar00rootroot00000000000000#ifndef CSV_WORLD_DIALOGUESPINBOX_H #define CSV_WORLD_DIALOGUESPINBOX_H #include #include namespace CSVWorld { class DialogueSpinBox : public QSpinBox { Q_OBJECT public: DialogueSpinBox(QWidget* parent = nullptr); 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: void focusInEvent(QFocusEvent* event) override; void focusOutEvent(QFocusEvent* event) override; void wheelEvent(QWheelEvent* event) override; }; } #endif // CSV_WORLD_DIALOGUESPINBOX_H openmw-openmw-0.49.0/apps/opencs/view/world/dialoguesubview.cpp000066400000000000000000001037721503074453300246670ustar00rootroot00000000000000#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 "../../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/nestedtableproxymodel.hpp" #include "../../model/world/record.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/prefs/state.hpp" #include "../widget/coloreditor.hpp" #include "../widget/droplineedit.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) { } void CSVWorld::NotEditableSubDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { QLabel* label = qobject_cast(editor); if (!label) return; QVariant v = index.data(Qt::EditRole); if (!v.isValid()) { v = index.data(Qt::DisplayRole); if (!v.isValid()) { return; } } 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)) { int data = v.toInt(); std::vector> enumNames(CSMWorld::Columns::getEnums(columnId)); label->setText(QString::fromUtf8(enumNames.at(data).second.c_str())); } else { label->setText(v.toString()); } } void CSVWorld::NotEditableSubDelegate::setModelData( QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { // not editable widgets will not save model data } void CSVWorld::NotEditableSubDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // does nothing } 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 { QLabel* label = new QLabel(parent); label->setTextInteractionFlags(Qt::TextSelectableByMouse); return label; } /* ==============================DialogueDelegateDispatcherProxy========================================== */ CSVWorld::DialogueDelegateDispatcherProxy::DialogueDelegateDispatcherProxy( QWidget* editor, CSMWorld::ColumnBase::Display display) : mEditor(editor) , mDisplay(display) { } void CSVWorld::DialogueDelegateDispatcherProxy::editorDataCommited() { if (mIndex.has_value()) { emit editorDataCommited(mEditor, mIndex.value(), mDisplay); } } void CSVWorld::DialogueDelegateDispatcherProxy::setIndex(const QModelIndex& index) { mIndex = index; } QWidget* CSVWorld::DialogueDelegateDispatcherProxy::getEditor() const { return mEditor; } /* ==============================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::CommandDelegate* CSVWorld::DialogueDelegateDispatcher::makeDelegate(CSMWorld::ColumnBase::Display display) { CommandDelegate* delegate = nullptr; std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt == mDelegates.end()) { delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, &mCommandDispatcher, mDocument, mParent); mDelegates.insert(std::make_pair(display, delegate)); } else { delegate = delegateIt->second; } return delegate; } 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 { 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()); } else { display = static_cast( mTable->headerData(index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); } QLabel* label = qobject_cast(editor); if (label) { mNotEditableDelegate.setEditorData(label, index); return; } std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { delegateIt->second->setEditorData(editor, index, true); } for (const auto& proxy : mProxys) { if (proxy->getEditor() == editor) { proxy->setIndex(index); } } } 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 { std::map::const_iterator delegateIt(mDelegates.find(display)); if (delegateIt != mDelegates.end()) { delegateIt->second->setModelData(editor, model, index); } } void CSVWorld::DialogueDelegateDispatcher::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { // Does nothing } QSize CSVWorld::DialogueDelegateDispatcher::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { return QSize(); // silencing warning, otherwise does nothing } QWidget* CSVWorld::DialogueDelegateDispatcher::makeEditor( CSMWorld::ColumnBase::Display display, const QModelIndex& index) { QVariant variant = index.data(); if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return nullptr; } } QWidget* editor = nullptr; if (!(mTable->flags(index) & Qt::ItemIsEditable)) { 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); DialogueDelegateDispatcherProxy* proxy = new DialogueDelegateDispatcherProxy(editor, display); // NOTE: For each entry in CSVWorld::CommandDelegate::createEditor() a corresponding entry // is required here if (qobject_cast(editor)) { connect(static_cast(editor), &CSVWidget::DropLineEdit::editingFinished, proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); connect(static_cast(editor), &CSVWidget::DropLineEdit::tableMimeDataDropped, proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { connect(static_cast(editor), &QCheckBox::stateChanged, proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { connect(static_cast(editor), &QPlainTextEdit::textChanged, proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { connect(static_cast(editor), qOverload(&QComboBox::currentIndexChanged), proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor) || qobject_cast(editor)) { connect(static_cast(editor), &QAbstractSpinBox::editingFinished, proxy, qOverload<>(&DialogueDelegateDispatcherProxy::editorDataCommited)); } else if (qobject_cast(editor)) { 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"); connect(proxy, qOverload( &DialogueDelegateDispatcherProxy::editorDataCommited), this, &DialogueDelegateDispatcher::editorDataCommited); mProxys.push_back(proxy); // deleted in the destructor } return editor; } CSVWorld::DialogueDelegateDispatcher::~DialogueDelegateDispatcher() { for (auto* proxy : mProxys) { 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)) { Q_ASSERT(mWidget != nullptr); Q_ASSERT(CSMWorld::ColumnBase::isId(display)); Q_ASSERT(mIdType != CSMWorld::UniversalId::Type_None); mWidget->setContextMenuPolicy(Qt::CustomContextMenu); connect(mWidget, &QWidget::customContextMenuRequested, this, &IdContextMenu::showContextMenu); mEditIdAction = new QAction(this); connect(mEditIdAction, &QAction::triggered, this, qOverload<>(&IdContextMenu::editIdRequest)); QLineEdit* lineEdit = qobject_cast(mWidget); if (lineEdit != nullptr) { mContextMenu = lineEdit->createStandardContextMenu(); } else { mContextMenu = new QMenu(mWidget); } } 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); QString value = ""; if (lineEdit != nullptr) { value = lineEdit->text(); } else if (label != nullptr) { value = label->text(); } return value; } void CSVWorld::IdContextMenu::addEditIdActionToMenu(const QString& text) { mEditIdAction->setText(text); if (mContextMenu->actions().isEmpty()) { mContextMenu->addAction(mEditIdAction); } else if (mContextMenu->actions().first() != mEditIdAction) { QAction* action = mContextMenu->actions().first(); mContextMenu->insertAction(action, mEditIdAction); mContextMenu->insertSeparator(action); } } void CSVWorld::IdContextMenu::removeEditIdActionFromMenu() { if (mContextMenu->actions().isEmpty()) { return; } if (mContextMenu->actions().first() == mEditIdAction) { mContextMenu->removeAction(mEditIdAction); if (!mContextMenu->actions().isEmpty() && mContextMenu->actions().first()->isSeparator()) { mContextMenu->removeAction(mContextMenu->actions().first()); } } } void CSVWorld::IdContextMenu::showContextMenu(const QPoint& pos) { QString value = getWidgetValue(); bool isExcludedId = mExcludedIds.find(value.toUtf8().constData()) != mExcludedIds.end(); if (!value.isEmpty() && !isExcludedId) { addEditIdActionToMenu("Edit '" + value + "'"); } else { removeEditIdActionFromMenu(); } if (!mContextMenu->actions().isEmpty()) { mContextMenu->exec(mWidget->mapToGlobal(pos)); } } void CSVWorld::IdContextMenu::editIdRequest() { CSMWorld::UniversalId editId(mIdType, getWidgetValue().toUtf8().constData()); emit editIdRequest(editId, ""); } /* =============================================================EditWidget===================================================== */ 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) { int idColumn = mTable->findColumnIndex(CSMWorld::Columns::ColumnId_Id); QString id = mTable->data(mTable->index(currentRow, idColumn)).toString(); 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, qOverload(&IdContextMenu::editIdRequest), this, &EditWidget::editIdRequest); } } CSVWorld::EditWidget::~EditWidget() { for (auto* model : mNestedModels) delete model; if (mDispatcher) delete mDispatcher; if (mNestedTableDispatcher) 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) { remake(row); } void CSVWorld::EditWidget::remake(int row) { if (mMainWidget) { QWidget* del = this->takeWidget(); del->deleteLater(); } mMainWidget = new QWidget(this); for (auto* model : mNestedModels) delete model; mNestedModels.clear(); if (mDispatcher) delete mDispatcher; 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 if (mWidgetMapper) delete mWidgetMapper; 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)); line->setFrameShape(QFrame::HLine); line->setFrameShadow(QFrame::Sunken); QFrame* line2 = new QFrame(mMainWidget); line2->setObjectName(QString::fromUtf8("line")); line2->setGeometry(QRect(320, 150, 118, 3)); 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(); mainLayout->addLayout(lockedLayout, QSizePolicy::Fixed); mainLayout->addWidget(line, 1); mainLayout->addLayout(unlockedLayout, QSizePolicy::Preferred); mainLayout->addWidget(line2, 1); 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; i < columns; ++i) { 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()); 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)); 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()); bool editable = true; bool fixedRows = false; QVariant v = mTable->index(row, i).data(); if (v.canConvert()) { assert(QString(v.typeName()) == "CSMWorld::ColumnBase::TableEditModes"); if (v.value() == CSMWorld::ColumnBase::TableEdit_None) editable = false; 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); 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); int headerHeight = table->horizontalHeader()->height(); 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); tablesLayout->addWidget(label); tablesLayout->addWidget(table); 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))); if (editor) { mWidgetMapper->addMapping(editor, i); QLabel* label = new QLabel(mTable->headerData(i, Qt::Horizontal).toString(), mMainWidget); label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); // 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); ++locked; } else { unlockedLayout->addWidget(label, unlocked, 0); unlockedLayout->addWidget(editor, unlocked, 1); ++unlocked; } if (CSMWorld::DisableTag::isDisableTag(mTable->index(row, i).data())) { editor->setEnabled(false); label->setEnabled(false); } createEditorContextMenu(editor, display, row); } } else // Flag_Dialogue_List { 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)); 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(); display = static_cast(displayRole); mNestedTableDispatcher->makeDelegate(display); // FIXME: assumed all columns are editable QWidget* editor = mNestedTableDispatcher->makeEditor(display, tree->index(0, col, tree->index(row, i))); if (editor) { 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); label->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); editor->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); unlockedLayout->addWidget(label, unlocked, 0); unlockedLayout->addWidget(editor, unlocked, 1); ++unlocked; if (CSMWorld::DisableTag::isDisableTag(tree->index(0, col, tree->index(row, i)).data())) { editor->setEnabled(false); label->setEnabled(false); } if (!isBlocked) createEditorContextMenu(editor, display, row); else editor->setEnabled(false); } } mNestedTableMapper->setCurrentModelIndex(tree->index(0, 0, tree->index(row, i))); } } } mWidgetMapper->setCurrentModelIndex(mTable->index(row, 0)); if (unlocked == 0) mainLayout->removeWidget(line); this->setWidget(mMainWidget); this->setWidgetResizable(true); } QVBoxLayout& CSVWorld::SimpleDialogueSubView::getMainLayout() { return *mMainLayout; } CSMWorld::IdTable& CSVWorld::SimpleDialogueSubView::getTable() { return *mTable; } CSMWorld::CommandDispatcher& CSVWorld::SimpleDialogueSubView::getCommandDispatcher() { return mCommandDispatcher; } CSVWorld::EditWidget& CSVWorld::SimpleDialogueSubView::getEditWidget() { return *mEditWidget; } 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())) { connect(mTable, &CSMWorld::IdTable::dataChanged, this, &SimpleDialogueSubView::dataChanged); connect(mTable, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &SimpleDialogueSubView::rowsAboutToBeRemoved); updateCurrentId(); QWidget* mainWidget = new QWidget(this); mMainLayout = new QVBoxLayout(mainWidget); setWidget(mainWidget); int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); 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)); connect(mEditWidget, &EditWidget::editIdRequest, this, &SimpleDialogueSubView::focusId); } 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); 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()); mEditWidget->setDisabled(state == CSMWorld::RecordBase::State_Deleted || locked); mCommandDispatcher.setEditLock(locked); } } void CSVWorld::SimpleDialogueSubView::dataChanged(const QModelIndex& index) { 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()) { CSMWorld::RecordBase::State state = static_cast(mTable->data(mTable->index(currentIndex.row(), 1)).toInt()); 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(); } else { 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->verticalScrollBar()->setValue(y); } } } void CSVWorld::SimpleDialogueSubView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(mTable->getModelIndex(getUniversalId().getId(), idColumn)); if (!currentIndex.isValid()) { return; } if (currentIndex.parent() == parent && currentIndex.row() >= start && currentIndex.row() <= end) { if (mEditWidget) { delete mEditWidget; mEditWidget = nullptr; } emit closeRequest(this); } } void CSVWorld::SimpleDialogueSubView::updateCurrentId() { std::vector selection; selection.push_back(getUniversalId().getId()); mCommandDispatcher.setSelection(selection); } void CSVWorld::DialogueSubView::addButtonBar() { if (mButtons) return; mButtons = new RecordButtonBar(getUniversalId(), getTable(), mBottom, &getCommandDispatcher(), this); getMainLayout().insertWidget(1, mButtons); // connections connect(mButtons, &RecordButtonBar::showPreview, this, &DialogueSubView::showPreview); connect(mButtons, &RecordButtonBar::viewRecord, this, &DialogueSubView::viewRecord); connect(mButtons, &RecordButtonBar::switchToRow, this, &DialogueSubView::switchToRow); 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) { // bottom box mBottom = new TableBottomBox(creatorFactory, document, id, this); connect(mBottom, &TableBottomBox::requestFocus, this, &DialogueSubView::requestFocus); // layout getMainLayout().addWidget(mBottom); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &DialogueSubView::settingChanged); CSMPrefs::get()["ID Dialogues"].update(); } void CSVWorld::DialogueSubView::setEditLock(bool locked) { SimpleDialogueSubView::setEditLock(locked); if (mButtons) mButtons->setEditLock(locked); } void CSVWorld::DialogueSubView::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "ID Dialogues/toolbar") { if (setting->isTrue()) { addButtonBar(); } else if (mButtons) { getMainLayout().removeWidget(mButtons); delete mButtons; mButtons = nullptr; } } } void CSVWorld::DialogueSubView::showPreview() { 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()) { emit focusId(CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Preview, getUniversalId().getId()), ""); } } void CSVWorld::DialogueSubView::viewRecord() { int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); QModelIndex currentIndex(getTable().getModelIndex(getUniversalId().getId(), idColumn)); if (currentIndex.isValid() && currentIndex.row() < getTable().rowCount()) { std::pair params = getTable().view(currentIndex.row()); if (params.first.getType() != CSMWorld::UniversalId::Type_None) emit focusId(params.first, params.second); } } 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 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)); updateCurrentId(); getEditWidget().remake(row); 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); } void CSVWorld::DialogueSubView::requestFocus(const std::string& id) { int idColumn = getTable().findColumnIndex(CSMWorld::Columns::ColumnId_Id); QModelIndex index = getTable().getModelIndex(id, idColumn); if (index.isValid()) switchToRow(index.row()); } openmw-openmw-0.49.0/apps/opencs/view/world/dialoguesubview.hpp000066400000000000000000000170521503074453300246670ustar00rootroot00000000000000#ifndef CSV_WORLD_DIALOGUESUBVIEW_H #define CSV_WORLD_DIALOGUESUBVIEW_H #include #include #include #include #include #include #include #include #include #ifndef Q_MOC_RUN #include "../doc/subview.hpp" #include "../../model/world/columnbase.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/universalid.hpp" #endif class QAbstractItemModel; class QAction; class QDataWidgetMapper; class QMenu; class QModelIndex; class QPainter; class QPoint; class QSize; class QVBoxLayout; class QWidget; namespace CSMWorld { class IdTable; class NestedTableProxyModel; } namespace CSMPrefs { class Setting; } namespace CSMDoc { class Document; } namespace CSVWorld { class CommandDelegate; class CreatorFactoryBase; class TableBottomBox; class NotEditableSubDelegate : public QAbstractItemDelegate { const CSMWorld::IdTable* mTable; public: NotEditableSubDelegate(const CSMWorld::IdTable* table, QObject* parent = nullptr); void setEditorData(QWidget* editor, 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; ///< does nothing QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing QWidget* createEditor( QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override; }; class DialogueDelegateDispatcherProxy : public QObject { Q_OBJECT QWidget* mEditor; CSMWorld::ColumnBase::Display mDisplay; std::optional mIndex; public: DialogueDelegateDispatcherProxy(QWidget* editor, CSMWorld::ColumnBase::Display display); QWidget* getEditor() const; public slots: void editorDataCommited(); void setIndex(const QModelIndex& index); signals: void editorDataCommited(QWidget* editor, const QModelIndex& index, CSMWorld::ColumnBase::Display display); }; class DialogueDelegateDispatcher : public QAbstractItemDelegate { Q_OBJECT std::map mDelegates; QObject* mParent; QAbstractItemModel* mTable; CSMWorld::CommandDispatcher& mCommandDispatcher; CSMDoc::Document& mDocument; NotEditableSubDelegate mNotEditableDelegate; std::vector mProxys; // 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(); CSVWorld::CommandDelegate* makeDelegate(CSMWorld::ColumnBase::Display display); 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 void setEditorData(QWidget* editor, 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; void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; ///< does nothing 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); }; /// A context menu with "Edit 'ID'" action for editors in the dialogue subview class IdContextMenu : public QObject { Q_OBJECT 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; QString getWidgetValue() const; void addEditIdActionToMenu(const QString& text); void removeEditIdActionFromMenu(); public: IdContextMenu(QWidget* widget, CSMWorld::ColumnBase::Display display); void excludeId(const std::string& id); private slots: void showContextMenu(const QPoint& pos); void editIdRequest(); 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); }; class SimpleDialogueSubView : public CSVDoc::SubView { Q_OBJECT EditWidget* mEditWidget; QVBoxLayout* mMainLayout; CSMWorld::IdTable* mTable; bool mLocked; const CSMDoc::Document& mDocument; CSMWorld::CommandDispatcher mCommandDispatcher; protected: QVBoxLayout& getMainLayout(); CSMWorld::IdTable& getTable(); CSMWorld::CommandDispatcher& getCommandDispatcher(); EditWidget& getEditWidget(); void updateCurrentId(); bool isLocked() const; public: SimpleDialogueSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; private slots: void dataChanged(const QModelIndex& index); ///\brief we need to care for deleting currently edited record void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); }; class RecordButtonBar; class DialogueSubView : public SimpleDialogueSubView { Q_OBJECT TableBottomBox* mBottom; RecordButtonBar* mButtons; private: void addButtonBar(); public: DialogueSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting = false); void setEditLock(bool locked) override; private slots: void settingChanged(const CSMPrefs::Setting* setting); void showPreview(); void viewRecord(); void switchToRow(int row); void requestFocus(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/dragdroputils.cpp000066400000000000000000000031311503074453300243400ustar00rootroot00000000000000#include "dragdroputils.hpp" #include #include #include #include "../../model/world/tablemimedata.hpp" const CSMWorld::TableMimeData* CSVWorld::DragDropUtils::getTableMimeData(const QDropEvent& event) { return dynamic_cast(event.mimeData()); } bool CSVWorld::DragDropUtils::canAcceptData(const QDropEvent& event, CSMWorld::ColumnBase::Display type) { const CSMWorld::TableMimeData* data = getTableMimeData(event); return data != nullptr && data->holdsType(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)); } CSMWorld::UniversalId CSVWorld::DragDropUtils::getAcceptedData( const QDropEvent& event, CSMWorld::ColumnBase::Display type) { if (canAcceptData(event, type)) { if (const CSMWorld::TableMimeData* data = getTableMimeData(event)) return data->returnMatching(type); } return CSMWorld::UniversalId::Type_None; } openmw-openmw-0.49.0/apps/opencs/view/world/dragdroputils.hpp000066400000000000000000000020361503074453300243500ustar00rootroot00000000000000#ifndef CSV_WORLD_DRAGDROPUTILS_HPP #define CSV_WORLD_DRAGDROPUTILS_HPP #include "../../model/world/columnbase.hpp" class QDropEvent; namespace CSMWorld { class TableMimeData; class UniversalId; } namespace CSVWorld { namespace DragDropUtils { const CSMWorld::TableMimeData* getTableMimeData(const QDropEvent& event); 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 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); ///< 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 } } #endif openmw-openmw-0.49.0/apps/opencs/view/world/dragrecordtable.cpp000066400000000000000000000077431503074453300246160ustar00rootroot00000000000000#include "dragrecordtable.hpp" #include #include #include #include #include "../../model/doc/document.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, const QModelIndex& index) { std::vector records = table.getDraggedRecords(); if (records.empty()) { return; } 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) { setAcceptDrops(true); } void CSVWorld::DragRecordTable::setEditLock(bool locked) { mEditLock = locked; } void CSVWorld::DragRecordTable::dragEnterEvent(QDragEnterEvent* event) { event->acceptProposedAction(); } void CSVWorld::DragRecordTable::dragMoveEvent(QDragMoveEvent* event) { QModelIndex index = indexAt(event->pos()); if (CSVWorld::DragDropUtils::canAcceptData(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isInfo(*event, getIndexDisplayType(index)) || CSVWorld::DragDropUtils::isTopicOrJournal(*event, getIndexDisplayType(index))) { if (index.flags() & Qt::ItemIsEditable) { event->accept(); return; } } event->ignore(); } 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); if (tableMimeData->fromDocument(mDocument)) { CSMWorld::UniversalId id = CSVWorld::DragDropUtils::getAcceptedData(*event, display); QVariant newIndexData = QString::fromUtf8(id.getId().c_str()); QVariant oldIndexData = index.data(Qt::EditRole); if (newIndexData != oldIndexData) { mDocument.getUndoStack().push(new CSMWorld::ModifyCommand(*model(), index, newIndexData)); } } } else if (CSVWorld::DragDropUtils::isInfo(*event, display) && event->source() == this) { 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 { Q_ASSERT(model() != nullptr); if (index.isValid()) { QVariant display = model()->headerData(index.column(), Qt::Horizontal, CSMWorld::ColumnBase::Role_Display); if (display.isValid()) { return static_cast(display.toInt()); } } 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); } openmw-openmw-0.49.0/apps/opencs/view/world/dragrecordtable.hpp000066400000000000000000000024711503074453300246140ustar00rootroot00000000000000#ifndef CSV_WORLD_DRAGRECORDTABLE_H #define CSV_WORLD_DRAGRECORDTABLE_H #include #include #include "../../model/world/columnbase.hpp" class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QModelIndex; class QObject; class QWidget; namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } namespace CSVWorld { class DragRecordTable : public QTableView { Q_OBJECT protected: CSMDoc::Document& mDocument; bool mEditLock; public: DragRecordTable(CSMDoc::Document& document, QWidget* parent = nullptr); virtual std::vector getDraggedRecords() const = 0; void setEditLock(bool locked); protected: void startDragFromTable(const DragRecordTable& table, const QModelIndex& index); void dragEnterEvent(QDragEnterEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; void dropEvent(QDropEvent* event) override; int sizeHintForColumn(int column) const override; private: CSMWorld::ColumnBase::Display getIndexDisplayType(const QModelIndex& index) const; signals: void moveRecordsFromSameTable(QDropEvent* event); void createNewInfoRecord(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/enumdelegate.cpp000066400000000000000000000126241503074453300241230ustar00rootroot00000000000000#include "enumdelegate.hpp" #include #include #include #include #include "../../model/world/commands.hpp" #include int CSVWorld::EnumDelegate::getValueIndex(const QModelIndex& index, int role) const { if (index.isValid() && index.data(role).isValid()) { int value = index.data(role).toInt(); int size = static_cast(mValues.size()); for (int i = 0; i < size; ++i) { if (value == mValues.at(i).first) { return i; } } } return -1; } void CSVWorld::EnumDelegate::setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { 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) { // do nothing if the value has not changed if (model->data(index).toInt() != iter->first) addCommands(model, index, iter->first); break; } } } void CSVWorld::EnumDelegate::addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const { 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) { } 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, 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); 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 { if (QComboBox* comboBox = dynamic_cast(editor)) { int role = Qt::EditRole; if (tryDisplay && !index.data(role).isValid()) { role = Qt::DisplayRole; if (!index.data(role).isValid()) { return; } } int valueIndex = getValueIndex(index, role); if (valueIndex != -1) { comboBox->setCurrentIndex(valueIndex); } } } void CSVWorld::EnumDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) { QStyleOptionViewItem itemOption(option); itemOption.text = mValues.at(valueIndex).second; QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &itemOption, painter); } } QSize CSVWorld::EnumDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { int valueIndex = getValueIndex(index); if (valueIndex != -1) { // Calculate the size hint as for a combobox. // So, the whole text is visible (isn't elided) when the editor is created QStyleOptionComboBox itemOption; itemOption.fontMetrics = option.fontMetrics; itemOption.palette = option.palette; itemOption.rect = option.rect; itemOption.state = option.state; 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); } return option.rect.size(); } CSVWorld::EnumDelegateFactory::EnumDelegateFactory(const char** names, bool allowNone) { assert(names); if (allowNone) add(-1, ""); for (int i = 0; names[i]; ++i) add(i, names[i]); } CSVWorld::EnumDelegateFactory::EnumDelegateFactory( const std::vector>& names, bool allowNone) { if (allowNone) add(-1, ""); int size = static_cast(names.size()); for (int i = 0; i < size; ++i) add(names[i].first, names[i].second.c_str()); } CSVWorld::CommandDelegate* CSVWorld::EnumDelegateFactory::makeDelegate( CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new EnumDelegate(mValues, dispatcher, document, parent); } void CSVWorld::EnumDelegateFactory::add(int value, const QString& name) { auto pair = std::make_pair(value, name); for (auto it = mValues.begin(); it != mValues.end(); ++it) { if (it->second > name) { mValues.insert(it, pair); return; } } mValues.emplace_back(value, name); } openmw-openmw-0.49.0/apps/opencs/view/world/enumdelegate.hpp000066400000000000000000000051521503074453300241260ustar00rootroot00000000000000#ifndef CSV_WORLD_ENUMDELEGATE_H #define CSV_WORLD_ENUMDELEGATE_H #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: std::vector> mValues; int getValueIndex(const QModelIndex& index, int role = Qt::DisplayRole) const; private: void setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override; virtual void addCommands(QAbstractItemModel* model, const QModelIndex& index, int type) const; public: EnumDelegate(const std::vector>& values, 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 = 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; }; class EnumDelegateFactory : public CommandDelegateFactory { protected: std::vector> mValues; 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 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. void add(int value, const QString& name); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/extendedcommandconfigurator.cpp000066400000000000000000000161011503074453300272400ustar00rootroot00000000000000#include "extendedcommandconfigurator.hpp" #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/data.hpp" #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, &CSMWorld::Data::idListChanged, this, &ExtendedCommandConfigurator::dataIdListChanged); mPerformButton = new QPushButton(this); mPerformButton->setDefault(true); mPerformButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(mPerformButton, &QPushButton::clicked, this, &ExtendedCommandConfigurator::performExtendedCommand); mCancelButton = new QPushButton("Cancel", this); mCancelButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); connect(mCancelButton, &QPushButton::clicked, this, &ExtendedCommandConfigurator::done); mTypeGroup = new QGroupBox(this); QGridLayout* groupLayout = new QGridLayout(mTypeGroup); groupLayout->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); mTypeGroup->setLayout(groupLayout); QHBoxLayout* mainLayout = new QHBoxLayout(this); mainLayout->setSizeConstraint(QLayout::SetNoConstraint); mainLayout->setContentsMargins(0, 0, 0, 0); mainLayout->addWidget(mTypeGroup); mainLayout->addWidget(mPerformButton); mainLayout->addWidget(mCancelButton); } void CSVWorld::ExtendedCommandConfigurator::configure( CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds) { mMode = mode; if (mMode != Mode_None) { mPerformButton->setText((mMode == Mode_Delete) ? "Extended Delete" : "Extended Revert"); mSelectedIds = selectedIds; mCommandDispatcher->setSelection(mSelectedIds); setupCheckBoxes(mCommandDispatcher->getExtendedTypes()); setupGroupLayout(); lockWidgets(mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::setEditLock(bool locked) { if (mEditLock != locked) { mEditLock = locked; lockWidgets(mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); setupGroupLayout(); } void CSVWorld::ExtendedCommandConfigurator::setupGroupLayout() { if (mMode == Mode_None) { return; } int groupWidth = mTypeGroup->geometry().width(); QGridLayout* layout = qobject_cast(mTypeGroup->layout()); // Find the optimal number of rows to place the checkboxes within the available space int divider = 1; do { while (layout->itemAt(0) != nullptr) { layout->removeItem(layout->itemAt(0)); } int counter = 0; int itemsPerRow = mNumUsedCheckBoxes / divider; CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (counter < mNumUsedCheckBoxes) { int row = counter / itemsPerRow; int column = counter - (counter / itemsPerRow) * itemsPerRow; layout->addWidget(current->first, row, column); } ++counter; } divider *= 2; } while (groupWidth < mTypeGroup->sizeHint().width() && divider <= mNumUsedCheckBoxes); } void CSVWorld::ExtendedCommandConfigurator::setupCheckBoxes(const std::vector& types) { // Make sure that we have enough checkboxes 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, &QCheckBox::stateChanged, this, &ExtendedCommandConfigurator::checkBoxStateChanged); mTypeCheckBoxes.insert(std::make_pair(checkBox, CSMWorld::UniversalId::Type_None)); } } // Set up the checkboxes int counter = 0; CheckBoxMap::iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (counter < numTypes) { CSMWorld::UniversalId type = types[counter]; current->first->setText(QString::fromUtf8(type.getTypeName().c_str())); current->first->setChecked(true); current->second = std::move(type); ++counter; } else { current->first->hide(); } } mNumChecked = mNumUsedCheckBoxes = numTypes; } void CSVWorld::ExtendedCommandConfigurator::lockWidgets(bool locked) { mPerformButton->setEnabled(!mEditLock && mNumChecked > 0); CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (int i = 0; current != end && i < mNumUsedCheckBoxes; ++current, ++i) { current->first->setEnabled(!mEditLock); } } void CSVWorld::ExtendedCommandConfigurator::performExtendedCommand() { std::vector types; CheckBoxMap::const_iterator current = mTypeCheckBoxes.begin(); CheckBoxMap::const_iterator end = mTypeCheckBoxes.end(); for (; current != end; ++current) { if (current->first->isChecked()) { types.push_back(current->second); } } mCommandDispatcher->setExtendedTypes(types); if (mMode == Mode_Delete) { mCommandDispatcher->executeExtendedDelete(); } else { mCommandDispatcher->executeExtendedRevert(); } emit done(); } void CSVWorld::ExtendedCommandConfigurator::checkBoxStateChanged(int state) { switch (state) { case Qt::Unchecked: --mNumChecked; break; case Qt::Checked: ++mNumChecked; break; case Qt::PartiallyChecked: // Not used break; } mPerformButton->setEnabled(mNumChecked > 0); } void CSVWorld::ExtendedCommandConfigurator::dataIdListChanged() { bool idsRemoved = false; for (int i = 0; i < static_cast(mSelectedIds.size()); ++i) { if (!mData.hasId(mSelectedIds[i])) { std::swap(mSelectedIds[i], mSelectedIds.back()); mSelectedIds.pop_back(); idsRemoved = true; --i; } } // If all selected IDs were removed, cancel the configurator if (mSelectedIds.empty()) { emit done(); return; } if (idsRemoved) { mCommandDispatcher->setSelection(mSelectedIds); } } openmw-openmw-0.49.0/apps/opencs/view/world/extendedcommandconfigurator.hpp000066400000000000000000000033401503074453300272460ustar00rootroot00000000000000#ifndef CSVWORLD_EXTENDEDCOMMANDCONFIGURATOR_HPP #define CSVWORLD_EXTENDEDCOMMANDCONFIGURATOR_HPP #include #include #include #include "../../model/world/universalid.hpp" class QPushButton; class QGroupBox; class QCheckBox; class QResizeEvent; namespace CSMDoc { class Document; } namespace CSMWorld { class CommandDispatcher; class Data; } 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(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/genericcreator.cpp000066400000000000000000000225631503074453300244630ustar00rootroot00000000000000#include "genericcreator.hpp" #include #include #include #include #include #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "idvalidator.hpp" void CSVWorld::GenericCreator::update() { mErrors = getErrors(); mCreate->setToolTip(QString::fromUtf8(mErrors.c_str())); mId->setToolTip(QString::fromUtf8(mErrors.c_str())); mCreate->setEnabled(mErrors.empty() && !mLocked); } void CSVWorld::GenericCreator::setManualEditing(bool enabled) { mId->setVisible(enabled); } void CSVWorld::GenericCreator::insertAtBeginning(QWidget* widget, bool stretched) { mLayout->insertWidget(0, widget, stretched ? 1 : 0); } void CSVWorld::GenericCreator::insertBeforeButtons(QWidget* widget, bool stretched) { mLayout->insertWidget(mLayout->count() - 2, widget, stretched ? 1 : 0); // Reset tab order relative to buttons. setTabOrder(widget, mCreate); setTabOrder(mCreate, mCancel); } std::string CSVWorld::GenericCreator::getId() const { return mId->text().toUtf8().constData(); } std::string CSVWorld::GenericCreator::getClonedId() const { return mClonedId; } std::string CSVWorld::GenericCreator::getIdValidatorResult() const { std::string errors; if (!mId->hasAcceptableInput()) errors = mValidator->getError(); return errors; } void CSVWorld::GenericCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const {} void CSVWorld::GenericCreator::pushCommand(std::unique_ptr command, const std::string& id) { mUndoStack.push(command.release()); } CSMWorld::Data& CSVWorld::GenericCreator::getData() const { return mData; } QUndoStack& CSVWorld::GenericCreator::getUndoStack() { return mUndoStack; } const CSMWorld::UniversalId& CSVWorld::GenericCreator::getCollectionId() const { return mListId; } std::string CSVWorld::GenericCreator::getNamespace() const { CSMWorld::Scope scope = CSMWorld::Scope_Content; if (mScope) { scope = static_cast(mScope->itemData(mScope->currentIndex()).toInt()); } else { if (mScopes & CSMWorld::Scope_Project) scope = CSMWorld::Scope_Project; else if (mScopes & CSMWorld::Scope_Session) scope = CSMWorld::Scope_Session; } switch (scope) { case CSMWorld::Scope_Content: return ""; case CSMWorld::Scope_Project: return "project::"; case CSMWorld::Scope_Session: return "session::"; } return ""; } void CSVWorld::GenericCreator::updateNamespace() { std::string namespace_ = getNamespace(); mValidator->setNamespace(namespace_); int index = mId->text().indexOf("::"); if (index == -1) { // no namespace in old text mId->setText(QString::fromUtf8(namespace_.c_str()) + mId->text()); } else { 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)); } } 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); } 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) // to IDs with general RecordList class (used for creators in Table subviews). CSMWorld::UniversalId::Type listParentType = CSMWorld::UniversalId::getParentType(mListId.getType()); if (listParentType != CSMWorld::UniversalId::Type_None) { mListId = listParentType; } mLayout = new QHBoxLayout; mLayout->setContentsMargins(0, 0, 0, 0); mId = new QLineEdit; mId->setValidator(mValidator = new IdValidator(relaxedIdRules, this)); mLayout->addWidget(mId, 1); mCreate = new QPushButton("Create"); mLayout->addWidget(mCreate); mCancel = new QPushButton("Cancel"); mLayout->addWidget(mCancel); setLayout(mLayout); connect(mCancel, &QPushButton::clicked, this, &GenericCreator::done); connect(mCreate, &QPushButton::clicked, this, &GenericCreator::create); connect(mId, &QLineEdit::textChanged, this, &GenericCreator::textChanged); connect(mId, &QLineEdit::returnPressed, this, &GenericCreator::inputReturnPressed); connect(&mData, &CSMWorld::Data::idListChanged, this, &GenericCreator::dataIdListChanged); } void CSVWorld::GenericCreator::setEditorMaxLength(int length) { mId->setMaxLength(length); } void CSVWorld::GenericCreator::setEditLock(bool locked) { mLocked = locked; update(); } void CSVWorld::GenericCreator::reset() { mCloneMode = false; mId->setText(""); update(); updateNamespace(); } std::string CSVWorld::GenericCreator::getErrors() const { std::string errors; if (!mId->hasAcceptableInput()) errors = mValidator->getError(); else if (mData.hasId(getId())) errors = "ID is already in use"; return errors; } void CSVWorld::GenericCreator::textChanged(const QString& text) { update(); } void CSVWorld::GenericCreator::inputReturnPressed() { if (mCreate->isEnabled()) { create(); } } void CSVWorld::GenericCreator::create() { if (!mLocked) { std::string id = getId(); std::unique_ptr command; if (mCloneMode) { command = std::make_unique( dynamic_cast(*mData.getTableModel(mListId)), mClonedId, id, mClonedType); } else { command = std::make_unique( dynamic_cast(*mData.getTableModel(mListId)), 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) { mCloneMode = true; mClonedId = originId; mClonedType = type; } void CSVWorld::GenericCreator::touch(const std::vector& ids) { // Combine multiple touch commands into one "macro" command mUndoStack.beginMacro("Touch Records"); CSMWorld::IdTable& table = dynamic_cast(*mData.getTableModel(mListId)); for (const CSMWorld::UniversalId& uid : ids) { CSMWorld::TouchCommand* touchCmd = new CSMWorld::TouchCommand(table, uid.getId()); mUndoStack.push(touchCmd); } // Execute mUndoStack.endMacro(); } void CSVWorld::GenericCreator::toggleWidgets(bool active) {} void CSVWorld::GenericCreator::focus() { mId->setFocus(); } void CSVWorld::GenericCreator::setScope(unsigned int scope) { mScopes = scope; int count = (mScopes & CSMWorld::Scope_Content) + (mScopes & CSMWorld::Scope_Project) + (mScopes & CSMWorld::Scope_Session); // scope selector widget if (count > 1) { 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."); if (mScopes & 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, "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, qOverload(&QComboBox::currentIndexChanged), this, &GenericCreator::scopeChanged); mScopeLabel = new QLabel("Scope", this); insertAtBeginning(mScopeLabel, false); mScope->setCurrentIndex(0); } else { delete mScope; mScope = nullptr; delete mScopeLabel; mScopeLabel = nullptr; } updateNamespace(); } void CSVWorld::GenericCreator::scopeChanged(int index) { update(); updateNamespace(); } void CSVWorld::GenericCreator::dataIdListChanged() { // If the original ID of cloned record was removed, cancel the creator if (mCloneMode && !mData.hasId(mClonedId)) { emit done(); } } openmw-openmw-0.49.0/apps/opencs/view/world/genericcreator.hpp000066400000000000000000000067351503074453300244730ustar00rootroot00000000000000#ifndef CSV_WORLD_GENERICCREATOR_H #define CSV_WORLD_GENERICCREATOR_H #include #include #include #include #include #include "../../model/world/universalid.hpp" #include "creator.hpp" class QPushButton; class QLineEdit; class QHBoxLayout; class QComboBox; class QLabel; class QUndoStack; namespace CSMWorld { class CreateCommand; class Data; } namespace CSVWorld { class IdValidator; 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; protected: bool mCloneMode; protected: void update(); virtual void setManualEditing(bool enabled); ///< Enable/disable manual ID editing (enabled by default). 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); virtual std::string getId() const; std::string getClonedId() 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 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; QUndoStack& getUndoStack(); const CSMWorld::UniversalId& getCollectionId() const; std::string getNamespace() const; void setEditorMaxLength(int length); private: void updateNamespace(); void addScope(const QString& name, CSMWorld::Scope scope, const QString& tooltip); public: GenericCreator( CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id, bool relaxedIdRules = false); void setEditLock(bool locked) override; void reset() override; void toggleWidgets(bool active = true) override; void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) 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. void setScope(unsigned int scope) override; /// Focus main input widget void focus() override; protected slots: /// \brief Create record if able to after Return key is pressed on input. void inputReturnPressed(); private slots: void textChanged(const QString& text); void create(); void scopeChanged(int index); void dataIdListChanged(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/globalcreator.cpp000066400000000000000000000015731503074453300243050ustar00rootroot00000000000000#include "globalcreator.hpp" #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" class QUndoStack; namespace CSVWorld { void GlobalCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { CSMWorld::IdTable* table = static_cast(getData().getTableModel(getCollectionId())); int index = table->findColumnIndex(CSMWorld::Columns::ColumnId_ValueType); int type = (int)ESM::VT_Float; command.addValue(index, type); } GlobalCreator::GlobalCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator(data, undoStack, id, true) { } } openmw-openmw-0.49.0/apps/opencs/view/world/globalcreator.hpp000066400000000000000000000011001503074453300242740ustar00rootroot00000000000000#ifndef CSV_WORLD_GLOBALCREATOR_H #define CSV_WORLD_GLOBALCREATOR_H #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); protected: void configureCreateCommand(CSMWorld::CreateCommand& command) const override; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/idcompletiondelegate.cpp000066400000000000000000000107371503074453300256500ustar00rootroot00000000000000#include "idcompletiondelegate.hpp" #include "../../model/world/idcompletionmanager.hpp" #include "../../model/world/infoselectwrapper.hpp" #include #include #include #include "../widget/droplineedit.hpp" 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 { return createEditor(parent, option, index, getDisplayTypeFromIndex(index)); } 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()) { return nullptr; } // The completer for InfoCondVar needs to return a completer based on the first column if (display == CSMWorld::ColumnBase::Display_InfoCondVar) { QModelIndex sibling = index.sibling(index.row(), 0); int conditionFunction = sibling.model()->data(sibling, Qt::EditRole).toInt(); switch (conditionFunction) { case ESM::DialogueCondition::Function_Global: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_GlobalVariable); } case ESM::DialogueCondition::Function_Journal: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Journal); } case ESM::DialogueCondition::Function_Item: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } case ESM::DialogueCondition::Function_Dead: case ESM::DialogueCondition::Function_NotId: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Referenceable); } case ESM::DialogueCondition::Function_NotFaction: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Faction); } case ESM::DialogueCondition::Function_NotClass: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Class); } case ESM::DialogueCondition::Function_NotRace: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Race); } case ESM::DialogueCondition::Function_NotCell: { return createEditor(parent, option, index, CSMWorld::ColumnBase::Display_Cell); } 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 } } 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 { return new IdCompletionDelegate(dispatcher, document, parent); } openmw-openmw-0.49.0/apps/opencs/view/world/idcompletiondelegate.hpp000066400000000000000000000023301503074453300256430ustar00rootroot00000000000000#ifndef CSV_WORLD_IDCOMPLETIONDELEGATE_HPP #define CSV_WORLD_IDCOMPLETIONDELEGATE_HPP #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; }; 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/idtypedelegate.cpp000077500000000000000000000023011503074453300244470ustar00rootroot00000000000000#include "idtypedelegate.hpp" #include "../../model/world/universalid.hpp" #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 < CSMWorld::UniversalId::NumberOfTypes; ++i) { CSMWorld::UniversalId id(static_cast(i)); 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 { return new IdTypeDelegate(mValues, mIcons, dispatcher, document, parent); } openmw-openmw-0.49.0/apps/opencs/view/world/idtypedelegate.hpp000077500000000000000000000016121503074453300244600ustar00rootroot00000000000000#ifndef IDTYPEDELEGATE_HPP #define IDTYPEDELEGATE_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); }; class IdTypeDelegateFactory : public DataDisplayDelegateFactory { 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. }; } #endif // REFIDTYPEDELEGATE_HPP openmw-openmw-0.49.0/apps/opencs/view/world/idvalidator.cpp000066400000000000000000000055231503074453300237660ustar00rootroot00000000000000#include "idvalidator.hpp" #include CSVWorld::IdValidator::IdValidator(bool relaxed, QObject* parent) : QValidator(parent) , mRelaxed(relaxed) { } QValidator::State CSVWorld::IdValidator::validate(QString& input, int& pos) const { mError.clear(); if (mRelaxed) { if (input.indexOf('"') != -1 || input.indexOf("::") != -1 || input.indexOf("#") != -1) return QValidator::Invalid; } else { if (input.isEmpty()) { mError = "Missing ID"; return QValidator::Intermediate; } bool first = true; bool scope = false; bool prevScope = false; QString::const_iterator iter = input.begin(); if (!mNamespace.empty()) { std::string namespace_ = input.left(static_cast(mNamespace.size())).toUtf8().constData(); if (Misc::StringUtils::lowerCase(namespace_) != mNamespace) return QValidator::Invalid; // incorrect namespace iter += namespace_.size(); first = false; prevScope = true; } else { int index = input.indexOf(":"); if (index != -1) { QString namespace_ = input.left(index); if (namespace_ == "project" || namespace_ == "session") return QValidator::Invalid; // reserved namespace } } for (; iter != input.end(); ++iter, first = false) { if (*iter == ':') { if (first) return QValidator::Invalid; // scope operator at the beginning if (scope) { scope = false; prevScope = true; } else { if (prevScope) return QValidator::Invalid; // sequence of two scope operators scope = true; } } else if (scope) return QValidator::Invalid; // incomplete scope operator else { prevScope = false; if (!iter->isPrint()) return QValidator::Invalid; } } if (scope) { mError = "ID ending with incomplete scope operator"; return QValidator::Intermediate; } if (prevScope) { mError = "ID ending with scope operator"; return QValidator::Intermediate; } } return QValidator::Acceptable; } void CSVWorld::IdValidator::setNamespace(const std::string& namespace_) { mNamespace = Misc::StringUtils::lowerCase(namespace_); } std::string CSVWorld::IdValidator::getError() const { return mError; } openmw-openmw-0.49.0/apps/opencs/view/world/idvalidator.hpp000066400000000000000000000016071503074453300237720ustar00rootroot00000000000000#ifndef CSV_WORLD_IDVALIDATOR_H #define CSV_WORLD_IDVALIDATOR_H #include #include namespace CSVWorld { class IdValidator : public QValidator { bool mRelaxed; std::string mNamespace; mutable std::string mError; 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; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/infocreator.cpp000066400000000000000000000135131503074453300237750ustar00rootroot00000000000000#include "infocreator.hpp" #include #include #include #include #include #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::InfoCreator::getId() const { std::string id = mTopic->text().toStdString(); size_t length = id.size(); // We want generated ids to be at most 31 + \0 characters id.resize(length + 32); id[length] = '#'; // Combine a random 32bit number with a random 64bit number for a max 30 character string quint32 gen32 = QRandomGenerator::global()->generate(); char* start = id.data() + length + 1; char* end = start + 10; // 2^32 is a 10 digit number auto result = std::to_chars(start, end, gen32); quint64 gen64 = QRandomGenerator::global()->generate64(); if (gen64) { // 0-pad the first number so 10 + 11 isn't the same as 101 + 1 std::fill(result.ptr, end, '0'); start = end; end = start + 20; // 2^64 is a 20 digit number result = std::to_chars(start, end, gen64); } id.resize(result.ptr - id.data()); return id; } void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { CSMWorld::IdTable& table = dynamic_cast(*getData().getTableModel(getCollectionId())); 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); } else { cloneCommand->setOverrideValue(table.findColumnIndex(CSMWorld::Columns::ColumnId_Topic), mTopic->text()); } } else { if (!cloneCommand) { 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) { // Determine if we're dealing with topics or journals. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Topic; QString labelText = "Topic"; if (getCollectionId().getType() == CSMWorld::UniversalId::Type_JournalInfos) { displayType = CSMWorld::ColumnBase::Display_Journal; labelText = "Journal"; } 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); setManualEditing(false); 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) { 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); mTopic->setText(infoTable.data(infoTable.getModelIndex(originId, topicColumn)).toString()); GenericCreator::cloneMode(originId, type); } void CSVWorld::InfoCreator::reset() { 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; const ESM::RefId topic = ESM::RefId::stringRefId(mTopic->text().toStdString()); if ((getCollectionId().getType() == CSMWorld::UniversalId::Type_TopicInfos ? getData().getTopics() : getData().getJournals()) .searchId(topic) == -1) { errors += "Invalid Topic ID"; } return errors; } 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 { return new InfoCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } openmw-openmw-0.49.0/apps/opencs/view/world/infocreator.hpp000066400000000000000000000032421503074453300240000ustar00rootroot00000000000000#ifndef CSV_WORLD_INFOCREATOR_H #define CSV_WORLD_INFOCREATOR_H #include "genericcreator.hpp" #include #include #include class QUndoStack; namespace CSMWorld { class IdCompletionManager; class CreateCommand; class Data; } namespace CSVWidget { class DropLineEdit; } namespace CSMDoc { class Document; } namespace CSVWorld { class InfoCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit* mTopic; std::string getId() const override; void configureCreateCommand(CSMWorld::CreateCommand& command) const override; public: 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 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; 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/landcreator.cpp000066400000000000000000000076641503074453300237720ustar00rootroot00000000000000#include "landcreator.hpp" #include #include #include #include #include #include #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/land.hpp" namespace CSVWorld { LandCreator::LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id) : GenericCreator(data, undoStack, id) , mXLabel(nullptr) , mYLabel(nullptr) , mX(nullptr) , mY(nullptr) { const int MaxInt = std::numeric_limits::max(); const int MinInt = std::numeric_limits::min(); setManualEditing(false); mXLabel = new QLabel("X: "); mX = new QSpinBox(); mX->setMinimum(MinInt); mX->setMaximum(MaxInt); insertBeforeButtons(mXLabel, false); insertBeforeButtons(mX, true); mYLabel = new QLabel("Y: "); mY = new QSpinBox(); mY->setMinimum(MinInt); mY->setMaximum(MaxInt); insertBeforeButtons(mYLabel, false); insertBeforeButtons(mY, true); 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) { GenericCreator::cloneMode(originId, type); int x = 0, y = 0; CSMWorld::Land::parseUniqueRecordId(originId, x, y); mX->setValue(x); mY->setValue(y); } void LandCreator::touch(const std::vector& ids) { // 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)); for (const CSMWorld::UniversalId& uid : ids) { CSMWorld::TouchLandCommand* touchCmd = new CSMWorld::TouchLandCommand(lands, ltexs, uid.getId()); getUndoStack().push(touchCmd); } // Execute getUndoStack().endMacro(); } void LandCreator::focus() { mX->setFocus(); } void LandCreator::reset() { GenericCreator::reset(); mX->setValue(0); mY->setValue(0); } std::string LandCreator::getErrors() const { if (getData().getLand().searchId(ESM::RefId::stringRefId(getId())) >= 0) return "A land with that name already exists."; return ""; } std::string LandCreator::getId() const { return CSMWorld::Land::createUniqueRecordId(mX->value(), mY->value()); } void LandCreator::pushCommand(std::unique_ptr command, const std::string& id) { if (mCloneMode) { 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()); getUndoStack().push(ltexCopy); getUndoStack().endMacro(); } else getUndoStack().push(command.release()); } void LandCreator::coordChanged(int value) { update(); } } openmw-openmw-0.49.0/apps/opencs/view/world/landcreator.hpp000066400000000000000000000021561503074453300237660ustar00rootroot00000000000000#ifndef CSV_WORLD_LANDCREATOR_H #define CSV_WORLD_LANDCREATOR_H #include "genericcreator.hpp" #include #include #include #include namespace CSMWorld { class CreateCommand; class Data; } class QLabel; class QSpinBox; namespace CSVWorld { class LandCreator : public GenericCreator { Q_OBJECT QLabel* mXLabel; QLabel* mYLabel; QSpinBox* mX; QSpinBox* mY; public: LandCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void touch(const std::vector& ids) override; void focus() override; void reset() override; std::string getErrors() const override; protected: std::string getId() const override; void pushCommand(std::unique_ptr command, const std::string& id) override; private slots: void coordChanged(int value); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/nestedtable.cpp000066400000000000000000000105071503074453300237540ustar00rootroot00000000000000#include "nestedtable.hpp" #include #include #include #include "../../model/prefs/shortcut.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, 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); setSelectionBehavior(QAbstractItemView::SelectRows); setSelectionMode(QAbstractItemView::ExtendedSelection); horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); verticalHeader()->hide(); int columns = model->columnCount(QModelIndex()); for (int i = 0; i < columns; ++i) { 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); setItemDelegateForColumn(i, delegate); } setModel(model); if (editable) { if (!fixedRows) { 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, &QAction::triggered, this, &NestedTable::removeRowActionTriggered); CSMPrefs::Shortcut* removeRowShortcut = new CSMPrefs::Shortcut("table-remove", this); removeRowShortcut->associateAction(mRemoveRowAction); } mEditIdAction = new TableEditIdAction(*this, this); connect(mEditIdAction, &QAction::triggered, this, &NestedTable::editCell); } } std::vector CSVWorld::NestedTable::getDraggedRecords() const { // No drag support for nested tables return std::vector(); } void CSVWorld::NestedTable::contextMenuEvent(QContextMenuEvent* event) { if (!mEditIdAction) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); QMenu menu(this); int currentRow = rowAt(event->y()); int currentColumn = columnAt(event->x()); if (mEditIdAction->isValidIdCell(currentRow, currentColumn)) { mEditIdAction->setCell(currentRow, currentColumn); menu.addAction(mEditIdAction); menu.addSeparator(); } if (mAddNewRowAction && mRemoveRowAction) { menu.addAction(mAddNewRowAction); menu.addAction(mRemoveRowAction); } menu.exec(event->globalPos()); } void CSVWorld::NestedTable::removeRowActionTriggered() { 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) { macro.push(new CSMWorld::DeleteNestedCommand(*(mModel->model()), mModel->getParentId(), selectionModel()->selectedRows()[i].row(), mModel->getParentColumn())); } } void CSVWorld::NestedTable::addNewRowActionTriggered() { int row = 0; if (!selectionModel()->selectedRows().empty()) row = selectionModel()->selectedRows().back().row() + 1; mDocument.getUndoStack().push( new CSMWorld::AddNestedCommand(*(mModel->model()), mModel->getParentId(), row, mModel->getParentColumn())); } void CSVWorld::NestedTable::editCell() { emit editRequest(mEditIdAction->getCurrentId(), ""); } openmw-openmw-0.49.0/apps/opencs/view/world/nestedtable.hpp000066400000000000000000000024031503074453300237550ustar00rootroot00000000000000#ifndef CSV_WORLD_NESTEDTABLE_H #define CSV_WORLD_NESTEDTABLE_H #include "dragrecordtable.hpp" #include #include class QAction; class QContextMenuEvent; namespace CSMWorld { class NestedTableProxyModel; class UniversalId; class CommandDispatcher; } namespace CSMDoc { class Document; } namespace CSVWorld { class TableEditIdAction; class NestedTable : public DragRecordTable { Q_OBJECT QAction* mAddNewRowAction; QAction* mRemoveRowAction; TableEditIdAction* mEditIdAction; CSMWorld::NestedTableProxyModel* mModel; CSMWorld::CommandDispatcher* mDispatcher; public: 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; private slots: void removeRowActionTriggered(); void addNewRowActionTriggered(); void editCell(); signals: void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/pathgridcreator.cpp000066400000000000000000000062671503074453300246540ustar00rootroot00000000000000#include "pathgridcreator.hpp" #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/columns.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::PathgridCreator::getId() const { return mCell->text().toUtf8().constData(); } CSMWorld::IdTable& CSVWorld::PathgridCreator::getPathgridsTable() const { 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) { setManualEditing(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. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Cell; mCell = new CSVWidget::DropLineEdit(displayType, this); mCell->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mCell, true); 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) { CSVWorld::GenericCreator::cloneMode(originId, type); // Look up cloned record in pathgrids table and set cell ID text. CSMWorld::IdTable& table = getPathgridsTable(); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_Id); mCell->setText(table.data(table.getModelIndex(originId, column)).toString()); } std::string CSVWorld::PathgridCreator::getErrors() const { const ESM::RefId cellId = ESM::RefId::stringRefId(getId()); // Check user input for any errors. std::string errors; if (cellId.empty()) { errors = "No cell ID selected"; } else if (getData().getPathgrids().searchId(cellId) > -1) { errors = "Pathgrid for selected cell ID already exists"; } else if (getData().getCells().searchId(cellId) == -1) { errors = "Cell with selected cell ID does not exist"; } return errors; } void CSVWorld::PathgridCreator::focus() { mCell->setFocus(); } void CSVWorld::PathgridCreator::reset() { CSVWorld::GenericCreator::reset(); mCell->setText(""); } void CSVWorld::PathgridCreator::cellChanged() { update(); } CSVWorld::Creator* CSVWorld::PathgridCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new PathgridCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } openmw-openmw-0.49.0/apps/opencs/view/world/pathgridcreator.hpp000066400000000000000000000035651503074453300246570ustar00rootroot00000000000000#ifndef PATHGRIDCREATOR_HPP #define PATHGRIDCREATOR_HPP #include #include "genericcreator.hpp" #include #include namespace CSMDoc { class Document; } namespace CSMWorld { class Data; class IdCompletionManager; class IdTable; } namespace CSVWidget { class DropLineEdit; } namespace CSVWorld { /// \brief Record creator for pathgrids. class PathgridCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit* mCell; private: /// \return Cell ID entered by user. std::string getId() const override; /// \return reference to table containing pathgrids. CSMWorld::IdTable& getPathgridsTable() const; public: 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; /// \return Error description for current user input. std::string getErrors() const override; /// \brief Set focus to cell ID input widget. void focus() override; /// \brief Clear cell ID input widget. void reset() override; private slots: /// \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; }; } #endif // PATHGRIDCREATOR_HPP openmw-openmw-0.49.0/apps/opencs/view/world/previewsubview.cpp000066400000000000000000000052311503074453300245460ustar00rootroot00000000000000#include "previewsubview.hpp" #include #include #include #include #include #include #include #include "../render/previewwidget.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #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; if (document.getData().getReferenceables().searchId(ESM::RefId::stringRefId(id.getId())) == -1) { std::string referenceableId = document.getData() .getReferences() .getRecord(ESM::RefId::stringRefId(id.getId())) .get() .mRefID.getRefIdString(); referenceableIdChanged(referenceableId); mScene = new CSVRender::PreviewWidget(document.getData(), id.getId(), false, this); } else mScene = new CSVRender::PreviewWidget(document.getData(), id.getId(), true, this); mScene->setExterior(true); CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar(48 + 6, this); CSVWidget::SceneToolMode* lightingTool = mScene->makeLightingSelector(toolbar); toolbar->addTool(lightingTool); layout->addWidget(toolbar, 0); layout->addWidget(mScene, 1); QWidget* widget = new QWidget; widget->setLayout(layout); setWidget(widget); 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) {} std::string CSVWorld::PreviewSubView::getTitle() const { return mTitle; } 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())); emit updateTitle(); } openmw-openmw-0.49.0/apps/opencs/view/world/previewsubview.hpp000066400000000000000000000013231503074453300245510ustar00rootroot00000000000000#ifndef CSV_WORLD_PREVIEWSUBVIEW_H #define CSV_WORLD_PREVIEWSUBVIEW_H #include "../doc/subview.hpp" #include #include namespace CSMDoc { class Document; } namespace CSVRender { class PreviewWidget; } namespace CSVWorld { class PreviewSubView : public CSVDoc::SubView { Q_OBJECT CSVRender::PreviewWidget* mScene; std::string mTitle; public: PreviewSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; std::string getTitle() const override; private slots: void referenceableIdChanged(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/recordbuttonbar.cpp000066400000000000000000000154001503074453300246560ustar00rootroot00000000000000#include "recordbuttonbar.hpp" #include #include #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); bool commandDisabled = !mCommandDispatcher || mLocked; mRevertButton->setDisabled(commandDisabled); mDeleteButton->setDisabled(commandDisabled || createAndDeleteDisabled); } void CSVWorld::RecordButtonBar::updatePrevNextButtons() { int rows = mTable.rowCount(); if (rows <= 1) { mPrevButton->setDisabled(true); mNextButton->setDisabled(true); } else if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) { mPrevButton->setDisabled(false); mNextButton->setDisabled(false); } else { int row = mTable.getModelIndex(mId.getId(), 0).row(); 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) { QHBoxLayout* buttonsLayout = new QHBoxLayout; buttonsLayout->setContentsMargins(0, 0, 0, 0); // left section 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(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(Misc::ScalableIcon::load(":edit-preview")); previewButton->setToolTip("Open a preview of this record"); buttonsLayout->addWidget(previewButton); connect(previewButton, &QToolButton::clicked, this, &RecordButtonBar::showPreview); } if (mTable.getFeatures() & CSMWorld::IdTable::Feature_View) { 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, &QToolButton::clicked, this, &RecordButtonBar::viewRecord); } // right section mCloneButton = new QToolButton(this); mCloneButton->setIcon(Misc::ScalableIcon::load(":edit-clone")); mCloneButton->setToolTip("Clone record"); buttonsLayout->addWidget(mCloneButton); 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(Misc::ScalableIcon::load(":edit-delete")); mDeleteButton->setToolTip("Delete record"); buttonsLayout->addWidget(mDeleteButton); mRevertButton = new QToolButton(this); mRevertButton->setIcon(Misc::ScalableIcon::load(":edit-undo")); mRevertButton->setToolTip("Revert record"); buttonsLayout->addWidget(mRevertButton); setLayout(buttonsLayout); // connections if (mBottom && mBottom->canCreateAndDelete()) { connect(mAddButton, &QToolButton::clicked, mBottom, &TableBottomBox::createRequest); connect(mCloneButton, &QToolButton::clicked, this, &RecordButtonBar::cloneRequest); } connect(mNextButton, &QToolButton::clicked, this, &RecordButtonBar::nextId); connect(mPrevButton, &QToolButton::clicked, this, &RecordButtonBar::prevId); if (mCommandDispatcher) { connect(mRevertButton, &QToolButton::clicked, mCommandDispatcher, &CSMWorld::CommandDispatcher::executeRevert); connect(mDeleteButton, &QToolButton::clicked, mCommandDispatcher, &CSMWorld::CommandDispatcher::executeDelete); } connect(&mTable, &CSMWorld::IdTable::rowsInserted, this, &RecordButtonBar::rowNumberChanged); connect(&mTable, &CSMWorld::IdTable::rowsRemoved, this, &RecordButtonBar::rowNumberChanged); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &RecordButtonBar::settingChanged); updateModificationButtons(); updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::setEditLock(bool locked) { mLocked = locked; updateModificationButtons(); } void CSVWorld::RecordButtonBar::universalIdChanged(const CSMWorld::UniversalId& id) { mId = id; updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "General Input/cycle") updatePrevNextButtons(); } void CSVWorld::RecordButtonBar::cloneRequest() { if (mBottom) { 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()); mBottom->cloneRequest(mId.getId(), type); } } void CSVWorld::RecordButtonBar::nextId() { int newRow = mTable.getModelIndex(mId.getId(), 0).row() + 1; if (newRow >= mTable.rowCount()) { if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) newRow = 0; else return; } emit switchToRow(newRow); } void CSVWorld::RecordButtonBar::prevId() { int newRow = mTable.getModelIndex(mId.getId(), 0).row() - 1; if (newRow < 0) { if (CSMPrefs::get()["General Input"]["cycle"].isTrue()) newRow = mTable.rowCount() - 1; else return; } emit switchToRow(newRow); } void CSVWorld::RecordButtonBar::rowNumberChanged(const QModelIndex& parent, int start, int end) { updatePrevNextButtons(); } openmw-openmw-0.49.0/apps/opencs/view/world/recordbuttonbar.hpp000066400000000000000000000035061503074453300246670ustar00rootroot00000000000000#ifndef CSV_WORLD_RECORDBUTTONBAR_H #define CSV_WORLD_RECORDBUTTONBAR_H #include #include "../../model/world/universalid.hpp" class QToolButton; class QModelIndex; namespace CSMWorld { class IdTable; class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class TableBottomBox; /// \brief Button bar for use in dialogue-type subviews /// /// Contains the following buttons: /// - next/prev /// - clone /// - add /// - delete /// - revert /// - preview (optional) /// - view (optional) class RecordButtonBar : public QWidget { 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; private: void updateModificationButtons(); void updatePrevNextButtons(); public: RecordButtonBar(const CSMWorld::UniversalId& id, CSMWorld::IdTable& table, TableBottomBox* bottomBox = nullptr, CSMWorld::CommandDispatcher* commandDispatcher = nullptr, QWidget* parent = nullptr); void setEditLock(bool locked); public slots: void universalIdChanged(const CSMWorld::UniversalId& id); private slots: void settingChanged(const CSMPrefs::Setting* setting); void cloneRequest(); void nextId(); void prevId(); void rowNumberChanged(const QModelIndex& parent, int start, int end); signals: void showPreview(); void viewRecord(); void switchToRow(int row); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/recordstatusdelegate.cpp000066400000000000000000000025641503074453300257030ustar00rootroot00000000000000#include "recordstatusdelegate.hpp" #include "../../model/world/columns.hpp" #include #include #include #include #include class QObject; namespace CSMDoc { 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); static const char* sIcons[] = { ":list-base", ":list-modified", ":list-added", ":list-removed", ":list-removed", 0 }; for (int i = 0; sIcons[i]; ++i) { auto& enumPair = enums.at(i); add(enumPair.first, enumPair.second.c_str(), sIcons[i]); } } openmw-openmw-0.49.0/apps/opencs/view/world/recordstatusdelegate.hpp000066400000000000000000000016651503074453300257110ustar00rootroot00000000000000#ifndef RECORDSTATUSDELEGATE_H #define RECORDSTATUSDELEGATE_H #include "datadisplaydelegate.hpp" 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); }; 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. }; } #endif // RECORDSTATUSDELEGATE_HPP openmw-openmw-0.49.0/apps/opencs/view/world/referenceablecreator.cpp000066400000000000000000000050451503074453300256250ustar00rootroot00000000000000#include "referenceablecreator.hpp" #include #include #include #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 { 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) { QLabel* label = new QLabel("Type", this); insertBeforeButtons(label, false); std::vector types = CSMWorld::UniversalId::listReferenceableTypes(); mType = new QComboBox(this); mType->setMaxVisibleItems(20); for (std::vector::const_iterator iter(types.begin()); iter != types.end(); ++iter) { CSMWorld::UniversalId id2(*iter, ""); mType->addItem(Misc::ScalableIcon::load(id2.getIcon().c_str()), id2.getTypeName().c_str(), static_cast(id2.getType())); } mType->model()->sort(0); insertBeforeButtons(mType, false); connect(mType, qOverload(&QComboBox::currentIndexChanged), this, &ReferenceableCreator::setType); } void CSVWorld::ReferenceableCreator::reset() { mType->setCurrentIndex(0); GenericCreator::reset(); } void CSVWorld::ReferenceableCreator::setType(int index) { // container items have name limit of 32 characters std::string text = mType->currentText().toStdString(); if (text == "Potion" || text == "Apparatus" || text == "Armor" || text == "Book" || text == "Clothing" || text == "Ingredient" || text == "ItemLevelledList" || text == "Light" || text == "Lockpick" || text == "Miscellaneous" || text == "Probe" || text == "Repair" || text == "Weapon") { GenericCreator::setEditorMaxLength(32); } else GenericCreator::setEditorMaxLength(32767); } void CSVWorld::ReferenceableCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { GenericCreator::cloneMode(originId, type); mType->setCurrentIndex(mType->findData(static_cast(type))); } void CSVWorld::ReferenceableCreator::toggleWidgets(bool active) { CSVWorld::GenericCreator::toggleWidgets(active); mType->setEnabled(active); } openmw-openmw-0.49.0/apps/opencs/view/world/referenceablecreator.hpp000066400000000000000000000016201503074453300256250ustar00rootroot00000000000000#ifndef CSV_WORLD_REFERENCEABLECREATOR_H #define CSV_WORLD_REFERENCEABLECREATOR_H #include "genericcreator.hpp" #include #include class QComboBox; class QObject; class QUndoStack; namespace CSMWorld { class CreateCommand; class Data; } namespace CSVWorld { class ReferenceableCreator : public GenericCreator { Q_OBJECT QComboBox* mType; private: void configureCreateCommand(CSMWorld::CreateCommand& command) const override; public: ReferenceableCreator(CSMWorld::Data& data, QUndoStack& undoStack, const CSMWorld::UniversalId& id); void reset() override; void cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) override; void toggleWidgets(bool active = true) override; private slots: void setType(int index); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/referencecreator.cpp000066400000000000000000000067021503074453300250020ustar00rootroot00000000000000#include "referencecreator.hpp" #include #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" std::string CSVWorld::ReferenceCreator::getId() const { return mId; } void CSVWorld::ReferenceCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const { // Set cellID int cellIdColumn = dynamic_cast(*getData().getTableModel(getCollectionId())) .findColumnIndex(CSMWorld::Columns::ColumnId_Cell); command.addValue(cellIdColumn, mCell->text()); } 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); // 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); setManualEditing(false); connect(mCell, &CSVWidget::DropLineEdit::textChanged, this, &ReferenceCreator::cellChanged); connect(mCell, &CSVWidget::DropLineEdit::returnPressed, this, &ReferenceCreator::inputReturnPressed); } void CSVWorld::ReferenceCreator::reset() { GenericCreator::reset(); mCell->setText(""); mId = getData().getReferences().getNewId(); } std::string CSVWorld::ReferenceCreator::getErrors() const { // We are ignoring errors coming from GenericCreator here, because the ID of the new // record is internal and requires neither user input nor verification. std::string errors; const ESM::RefId cell = ESM::RefId::stringRefId(mCell->text().toStdString()); if (cell.empty()) errors += "Missing Cell ID"; else if (getData().getCells().searchId(cell) == -1) errors += "Invalid Cell ID"; return errors; } void CSVWorld::ReferenceCreator::focus() { mCell->setFocus(); } void CSVWorld::ReferenceCreator::cellChanged() { update(); } void CSVWorld::ReferenceCreator::cloneMode(const std::string& originId, const CSMWorld::UniversalId::Type type) { CSMWorld::IdTable& referenceTable = dynamic_cast(*getData().getTableModel(CSMWorld::UniversalId::Type_References)); int cellIdColumn = referenceTable.findColumnIndex(CSMWorld::Columns::ColumnId_Cell); mCell->setText(referenceTable.data(referenceTable.getModelIndex(originId, cellIdColumn)).toString()); CSVWorld::GenericCreator::cloneMode(originId, type); cellChanged(); // otherwise ok button will remain disabled } CSVWorld::Creator* CSVWorld::ReferenceCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new ReferenceCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } openmw-openmw-0.49.0/apps/opencs/view/world/referencecreator.hpp000066400000000000000000000032111503074453300247770ustar00rootroot00000000000000#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 { class DropLineEdit; } namespace CSVWorld { class ReferenceCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit* mCell; std::string mId; private: std::string getId() const override; void configureCreateCommand(CSMWorld::CreateCommand& command) const override; public: 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 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. /// Focus main input widget void focus() override; private slots: 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. }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/regionmap.cpp000066400000000000000000000307661503074453300234540ustar00rootroot00000000000000#include "regionmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.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/regionmap.hpp" #include "../../model/world/tablemimedata.hpp" #include "../../model/world/universalid.hpp" void CSVWorld::RegionMap::contextMenuEvent(QContextMenuEvent* event) { QMenu menu(this); if (getUnselectedCells().size() > 0) menu.addAction(mSelectAllAction); if (selectionModel()->selectedIndexes().size() > 0) menu.addAction(mClearSelectionAction); if (getMissingRegionCells().size() > 0) menu.addAction(mSelectRegionsAction); int selectedNonExistentCells = getSelectedCells(false, true).size(); if (selectedNonExistentCells > 0) { if (selectedNonExistentCells == 1) mCreateCellsAction->setText("Create one Cell"); else { std::ostringstream stream; stream << "Create " << selectedNonExistentCells << " cells"; mCreateCellsAction->setText(QString::fromUtf8(stream.str().c_str())); } menu.addAction(mCreateCellsAction); } if (getSelectedCells().size() > 0) { if (!mRegionId.empty()) { mSetRegionAction->setText(QString::fromUtf8(("Set Region to " + mRegionId).c_str())); menu.addAction(mSetRegionAction); } menu.addAction(mUnsetRegionAction); menu.addAction(mViewInTableAction); } if (selectionModel()->selectedIndexes().size() > 0) menu.addAction(mViewAction); menu.exec(event->globalPos()); } QModelIndexList CSVWorld::RegionMap::getUnselectedCells() const { const QAbstractItemModel* model = QTableView::model(); int rows = model->rowCount(); int columns = model->columnCount(); QModelIndexList selected = selectionModel()->selectedIndexes(); std::sort(selected.begin(), selected.end()); QModelIndexList all; for (int y = 0; y < rows; ++y) for (int x = 0; x < columns; ++x) { 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()); QModelIndexList 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 { const QAbstractItemModel* model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); QModelIndexList list; for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { bool exists = model->data(*iter, Qt::BackgroundRole) != QBrush(Qt::DiagCrossPattern); if ((exists && existent) || (!exists && nonExistent)) list.push_back(*iter); } return list; } QModelIndexList CSVWorld::RegionMap::getMissingRegionCells() const { const QAbstractItemModel* model = QTableView::model(); QModelIndexList selected = selectionModel()->selectedIndexes(); std::set regions; for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { std::string region = model->data(*iter, CSMWorld::RegionMap::Role_Region).toString().toUtf8().constData(); if (!region.empty()) regions.insert(region); } QModelIndexList list; QModelIndexList unselected = getUnselectedCells(); for (QModelIndexList::const_iterator iter(unselected.begin()); iter != unselected.end(); ++iter) { 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); } return list; } void CSVWorld::RegionMap::setRegion(const std::string& regionId) { QModelIndexList selected = getSelectedCells(); QAbstractItemModel* regionModel = model(); CSMWorld::IdTable* cellsModel = &dynamic_cast(*mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); QString regionId2 = QString::fromUtf8(regionId.c_str()); CSMWorld::CommandMacro macro(mDocument.getUndoStack(), selected.size() > 1 ? tr("Set Region") : ""); for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { std::string cellId = regionModel->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); QModelIndex index = cellsModel->getModelIndex(cellId, cellsModel->findColumnIndex(CSMWorld::Columns::ColumnId_Region)); macro.push(new CSMWorld::ModifyCommand(*cellsModel, index, regionId2)); } } CSVWorld::RegionMap::RegionMap(const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget* parent) : DragRecordTable(document, parent) { verticalHeader()->hide(); horizontalHeader()->hide(); setSelectionMode(QAbstractItemView::ExtendedSelection); setModel(document.getData().getTableModel(universalId)); resizeColumnsToContents(); resizeRowsToContents(); 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, &QAction::triggered, this, &RegionMap::clearSelection); addAction(mClearSelectionAction); 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, &QAction::triggered, this, &RegionMap::createCells); addAction(mCreateCellsAction); 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, &QAction::triggered, this, &RegionMap::unsetRegion); addAction(mUnsetRegionAction); 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, &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); } void CSVWorld::RegionMap::clearSelection() { selectionModel()->clearSelection(); } void CSVWorld::RegionMap::selectRegions() { QModelIndexList unselected = getMissingRegionCells(); for (QModelIndexList::const_iterator iter(unselected.begin()); iter != unselected.end(); ++iter) selectionModel()->select(*iter, QItemSelectionModel::Select); } void CSVWorld::RegionMap::createCells() { if (mEditLock) return; QModelIndexList selected = getSelectedCells(false, true); CSMWorld::IdTable* cellsModel = &dynamic_cast(*mDocument.getData().getTableModel(CSMWorld::UniversalId::Type_Cells)); CSMWorld::CommandMacro macro(mDocument.getUndoStack(), selected.size() > 1 ? tr("Create cells") : ""); for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); macro.push(new CSMWorld::CreateCommand(*cellsModel, cellId)); } } void CSVWorld::RegionMap::setRegion() { if (mEditLock) return; setRegion(mRegionId); } void CSVWorld::RegionMap::unsetRegion() { if (mEditLock) return; setRegion(""); } void CSVWorld::RegionMap::view() { std::ostringstream hint; hint << "c:"; QModelIndexList selected = selectionModel()->selectedIndexes(); bool first = true; for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); if (first) first = false; else hint << "; "; hint << cellId; } emit editRequest( CSMWorld::UniversalId(CSMWorld::UniversalId::Type_Scene, ESM::Cell::sDefaultWorldspaceId.getValue()), hint.str()); } void CSVWorld::RegionMap::viewInTable() { std::ostringstream hint; hint << "f:!or("; QModelIndexList selected = getSelectedCells(); bool first = true; for (QModelIndexList::const_iterator iter(selected.begin()); iter != selected.end(); ++iter) { std::string cellId = model()->data(*iter, CSMWorld::RegionMap::Role_CellId).toString().toUtf8().constData(); if (first) first = false; else hint << ","; hint << "string(ID,\"" << cellId << "\")"; } hint << ")"; emit editRequest(CSMWorld::UniversalId::Type_Cells, hint.str()); } void CSVWorld::RegionMap::mouseMoveEvent(QMouseEvent* event) { startDragFromTable(*this, indexAt(event->pos())); } 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()); } 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()); } return ids; } void CSVWorld::RegionMap::dragMoveEvent(QDragMoveEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (mime != nullptr && (mime->holdsType(CSMWorld::UniversalId::Type_Region))) { event->accept(); return; } event->ignore(); } 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()); 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)); QAbstractItemModel* regionModel = model(); 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()); 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()))); mRegionId = record.getId(); } } openmw-openmw-0.49.0/apps/opencs/view/world/regionmap.hpp000066400000000000000000000041601503074453300234460ustar00rootroot00000000000000#ifndef CSV_WORLD_REGIONMAP_H #define CSV_WORLD_REGIONMAP_H #include #include #include #include "./dragrecordtable.hpp" class QAction; class QContextMenuEvent; class QDropEvent; class QMouseEvent; class QObject; class QWidget; namespace CSMDoc { class Document; } namespace CSMWorld { class UniversalId; } 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; private: void contextMenuEvent(QContextMenuEvent* event) override; 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 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 mouseMoveEvent(QMouseEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; void dropEvent(QDropEvent* event) override; public: RegionMap(const CSMWorld::UniversalId& universalId, CSMDoc::Document& document, QWidget* parent = nullptr); std::vector getDraggedRecords() const override; signals: void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); private slots: void selectAll() override; void clearSelection(); void selectRegions(); void createCells(); void setRegion(); void unsetRegion(); void view(); void viewInTable(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/regionmapsubview.cpp000066400000000000000000000012241503074453300250440ustar00rootroot00000000000000#include "regionmapsubview.hpp" #include #include "regionmap.hpp" CSVWorld::RegionMapSubView::RegionMapSubView(CSMWorld::UniversalId universalId, CSMDoc::Document& document) : CSVDoc::SubView(universalId) { mRegionMap = new RegionMap(universalId, document, this); setWidget(mRegionMap); connect(mRegionMap, &RegionMap::editRequest, this, &RegionMapSubView::editRequest); } void CSVWorld::RegionMapSubView::setEditLock(bool locked) { mRegionMap->setEditLock(locked); } void CSVWorld::RegionMapSubView::editRequest(const CSMWorld::UniversalId& id, const std::string& hint) { focusId(id, hint); } openmw-openmw-0.49.0/apps/opencs/view/world/regionmapsubview.hpp000066400000000000000000000012021503074453300250450ustar00rootroot00000000000000#ifndef CSV_WORLD_REGIONMAPSUBVIEW_H #define CSV_WORLD_REGIONMAPSUBVIEW_H #include #include "../doc/subview.hpp" #include namespace CSMDoc { class Document; } namespace CSVWorld { class RegionMap; class RegionMapSubView : public CSVDoc::SubView { Q_OBJECT RegionMap* mRegionMap; public: RegionMapSubView(CSMWorld::UniversalId universalId, CSMDoc::Document& document); void setEditLock(bool locked) override; private slots: void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/scenesubview.cpp000066400000000000000000000200461503074453300241630ustar00rootroot00000000000000#include "scenesubview.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/cellselection.hpp" #include "../filter/filterbox.hpp" #include "../render/pagedworldspacewidget.hpp" #include "../render/unpagedworldspacewidget.hpp" #include "../widget/scenetoolbar.hpp" #include "../widget/scenetoolmode.hpp" #include "../widget/scenetoolrun.hpp" #include "../widget/scenetooltoggle2.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) { QVBoxLayout* layout = new QVBoxLayout; layout->addWidget(mBottom = new TableBottomBox(NullCreatorFactory(), document, id, this), 0); mLayout->setContentsMargins(QMargins(0, 0, 0, 0)); CSVRender::WorldspaceWidget* worldspaceWidget = nullptr; widgetType whatWidget; if (Misc::StringUtils::ciEqual(id.getId(), ESM::Cell::sDefaultWorldspaceId.getValue())) { whatWidget = widget_Paged; CSVRender::PagedWorldspaceWidget* newWidget = new CSVRender::PagedWorldspaceWidget(this, document); worldspaceWidget = newWidget; makeConnections(newWidget); } else { whatWidget = widget_Unpaged; CSVRender::UnpagedWorldspaceWidget* newWidget = new CSVRender::UnpagedWorldspaceWidget(id.getId(), document, this); worldspaceWidget = newWidget; makeConnections(newWidget); } replaceToolbarAndWorldspace(worldspaceWidget, makeToolbar(worldspaceWidget, whatWidget)); layout->insertLayout(0, mLayout, 1); CSVFilter::FilterBox* filterBox = new CSVFilter::FilterBox(document.getData(), this); layout->insertWidget(0, filterBox); QWidget* widget = new QWidget; widget->setLayout(layout); setWidget(widget); } void CSVWorld::SceneSubView::makeConnections(CSVRender::UnpagedWorldspaceWidget* widget) { connect(widget, &CSVRender::UnpagedWorldspaceWidget::closeRequest, this, qOverload<>(&SceneSubView::closeRequest)); connect(widget, &CSVRender::UnpagedWorldspaceWidget::dataDropped, this, &SceneSubView::handleDrop); connect(widget, &CSVRender::UnpagedWorldspaceWidget::cellChanged, this, qOverload(&SceneSubView::cellSelectionChanged)); connect(widget, &CSVRender::UnpagedWorldspaceWidget::requestFocus, this, &SceneSubView::requestFocus); } void CSVWorld::SceneSubView::makeConnections(CSVRender::PagedWorldspaceWidget* widget) { connect(widget, &CSVRender::PagedWorldspaceWidget::closeRequest, this, qOverload<>(&SceneSubView::closeRequest)); connect(widget, &CSVRender::PagedWorldspaceWidget::dataDropped, this, &SceneSubView::handleDrop); connect(widget, &CSVRender::PagedWorldspaceWidget::cellSelectionChanged, this, qOverload(&SceneSubView::cellSelectionChanged)); connect(widget, &CSVRender::PagedWorldspaceWidget::requestFocus, this, &SceneSubView::requestFocus); } CSVWidget::SceneToolbar* CSVWorld::SceneSubView::makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type) { CSVWidget::SceneToolbar* toolbar = new CSVWidget::SceneToolbar(48 + 6, this); CSVWidget::SceneToolMode* navigationTool = widget->makeNavigationSelector(toolbar); toolbar->addTool(navigationTool); CSVWidget::SceneToolMode* lightingTool = widget->makeLightingSelector(toolbar); toolbar->addTool(lightingTool); CSVWidget::SceneToolToggle2* sceneVisibilityTool = widget->makeSceneVisibilitySelector(toolbar); toolbar->addTool(sceneVisibilityTool); if (type == widget_Paged) { CSVWidget::SceneToolToggle2* controlVisibilityTool = dynamic_cast(*widget).makeControlVisibilitySelector(toolbar); toolbar->addTool(controlVisibilityTool); } CSVWidget::SceneToolRun* runTool = widget->makeRunTool(toolbar); toolbar->addTool(runTool); toolbar->addTool(widget->makeEditModeSelector(toolbar), runTool); return toolbar; } void CSVWorld::SceneSubView::setEditLock(bool locked) { mScene->setEditLock(locked); } void CSVWorld::SceneSubView::setStatusBar(bool show) { mBottom->setStatusBar(show); } void CSVWorld::SceneSubView::useHint(const std::string& hint) { mScene->useViewHint(hint); } std::string CSVWorld::SceneSubView::getTitle() const { return mTitle; } void CSVWorld::SceneSubView::cellSelectionChanged(const CSMWorld::UniversalId& id) { setUniversalId(id); mTitle = "Scene: " + getUniversalId().getId(); setWindowTitle(QString::fromUtf8(mTitle.c_str())); emit updateTitle(); } void CSVWorld::SceneSubView::cellSelectionChanged(const CSMWorld::CellSelection& selection) { 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) stream << " (empty)"; else if (size == 1) { stream << " (" << *selection.begin() << ")"; } else { stream << " (" << selection.getCentre() << " and " << size - 1 << " more "; if (size > 1) stream << "cells around it)"; else stream << "cell around it)"; } mTitle = stream.str(); setWindowTitle(QString::fromUtf8(mTitle.c_str())); emit updateTitle(); } 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); switch (mScene->getDropRequirements(type)) { case CSVRender::WorldspaceWidget::canHandle: mScene->handleDrop(universalIdData, type); break; case CSVRender::WorldspaceWidget::needPaged: pagedNewWidget = new CSVRender::PagedWorldspaceWidget(this, mDocument); toolbar = makeToolbar(pagedNewWidget, widget_Paged); makeConnections(pagedNewWidget); replaceToolbarAndWorldspace(pagedNewWidget, toolbar); mScene->handleDrop(universalIdData, type); break; case CSVRender::WorldspaceWidget::needUnpaged: unPagedNewWidget = new CSVRender::UnpagedWorldspaceWidget(universalIdData.begin()->getId(), mDocument, this); toolbar = makeToolbar(unPagedNewWidget, widget_Unpaged); makeConnections(unPagedNewWidget); replaceToolbarAndWorldspace(unPagedNewWidget, toolbar); cellSelectionChanged(*(universalIdData.begin())); break; case CSVRender::WorldspaceWidget::ignored: return; } } void CSVWorld::SceneSubView::replaceToolbarAndWorldspace( CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar) { assert(mLayout); if (mScene) { mLayout->removeWidget(mScene); mScene->deleteLater(); } if (mToolbar) { mLayout->removeWidget(mToolbar); mToolbar->deleteLater(); } mScene = widget; mToolbar = toolbar; 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); mScene->selectDefaultNavigationMode(); setFocusProxy(mScene); } openmw-openmw-0.49.0/apps/opencs/view/world/scenesubview.hpp000066400000000000000000000035601503074453300241720ustar00rootroot00000000000000#ifndef CSV_WORLD_SCENESUBVIEW_H #define CSV_WORLD_SCENESUBVIEW_H #include #include #include #include #include "../doc/subview.hpp" namespace CSMWorld { class CellSelection; } namespace CSMDoc { class Document; } namespace CSVRender { class WorldspaceWidget; class PagedWorldspaceWidget; class UnpagedWorldspaceWidget; } namespace CSVWidget { class SceneToolbar; } namespace CSVWorld { class TableBottomBox; class SceneSubView : public CSVDoc::SubView { Q_OBJECT TableBottomBox* mBottom; CSVRender::WorldspaceWidget* mScene; QHBoxLayout* mLayout; CSMDoc::Document& mDocument; CSVWidget::SceneToolbar* mToolbar; std::string mTitle; public: SceneSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; void setStatusBar(bool show) override; void useHint(const std::string& hint) override; std::string getTitle() const override; private: void makeConnections(CSVRender::PagedWorldspaceWidget* widget); void makeConnections(CSVRender::UnpagedWorldspaceWidget* widget); void replaceToolbarAndWorldspace(CSVRender::WorldspaceWidget* widget, CSVWidget::SceneToolbar* toolbar); enum widgetType { widget_Paged, widget_Unpaged }; CSVWidget::SceneToolbar* makeToolbar(CSVRender::WorldspaceWidget* widget, widgetType type); private slots: void cellSelectionChanged(const CSMWorld::CellSelection& selection); void cellSelectionChanged(const CSMWorld::UniversalId& id); void handleDrop(const std::vector& data); signals: void requestFocus(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/scriptedit.cpp000066400000000000000000000345471503074453300236460ustar00rootroot00000000000000#include "scriptedit.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../../model/doc/document.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) { ++mEdit.mChangeLocked; } CSVWorld::ScriptEdit::ChangeLock::~ChangeLock() { --mEdit.mChangeLocked; } bool CSVWorld::ScriptEdit::event(QEvent* event) { // ignore undo and redo shortcuts if (event->type() == QEvent::ShortcutOverride) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->matches(QKeySequence::Undo) || keyEvent->matches(QKeySequence::Redo)) return true; } 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) , mWhiteListQuotes("^[a-zA-Z_][a-zA-Z0-9_]*$") { wrapLines(false); setTabWidth(); setUndoRedoEnabled(false); // we use OpenCS-wide undo/redo instead mAllowedTypes << CSMWorld::UniversalId::Type_Journal << CSMWorld::UniversalId::Type_Global << CSMWorld::UniversalId::Type_Topic << CSMWorld::UniversalId::Type_Sound << CSMWorld::UniversalId::Type_Spell << CSMWorld::UniversalId::Type_Cell << CSMWorld::UniversalId::Type_Referenceable << CSMWorld::UniversalId::Type_Activator << CSMWorld::UniversalId::Type_Potion << CSMWorld::UniversalId::Type_Apparatus << CSMWorld::UniversalId::Type_Armor << CSMWorld::UniversalId::Type_Book << CSMWorld::UniversalId::Type_Clothing << CSMWorld::UniversalId::Type_Container << CSMWorld::UniversalId::Type_Creature << CSMWorld::UniversalId::Type_Door << CSMWorld::UniversalId::Type_Ingredient << CSMWorld::UniversalId::Type_CreatureLevelledList << CSMWorld::UniversalId::Type_ItemLevelledList << CSMWorld::UniversalId::Type_Light << CSMWorld::UniversalId::Type_Lockpick << CSMWorld::UniversalId::Type_Miscellaneous << CSMWorld::UniversalId::Type_Npc << CSMWorld::UniversalId::Type_Probe << CSMWorld::UniversalId::Type_Repair << CSMWorld::UniversalId::Type_Static << CSMWorld::UniversalId::Type_Weapon << CSMWorld::UniversalId::Type_Script << CSMWorld::UniversalId::Type_Region; connect(this, &ScriptEdit::cursorPositionChanged, this, &ScriptEdit::markOccurrences); mCommentAction = new QAction(tr("Comment Selection"), this); connect(mCommentAction, &QAction::triggered, this, &ScriptEdit::commentSelection); CSMPrefs::Shortcut* commentShortcut = new CSMPrefs::Shortcut("script-editor-comment", this); commentShortcut->associateAction(mCommentAction); 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()); connect(&document.getData(), &CSMWorld::Data::idListChanged, this, &ScriptEdit::idListChanged); connect(&mUpdateTimer, &QTimer::timeout, this, &ScriptEdit::updateHighlighting); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptEdit::settingChanged); { ChangeLock lock(*this); CSMPrefs::get()["Scripts"].update(); } mUpdateTimer.setSingleShot(true); // TODO: provide a font selector dialogue mMonoFont.setStyleHint(QFont::TypeWriter); mLineNumberArea = new LineNumberArea(this); updateLineNumberAreaWidth(0); connect(this, &ScriptEdit::blockCountChanged, this, &ScriptEdit::updateLineNumberAreaWidth); connect(this, &ScriptEdit::updateRequest, this, &ScriptEdit::updateLineNumberArea); updateHighlighting(); } void CSVWorld::ScriptEdit::showLineNum(bool show) { if (show != mShowLineNum) { mShowLineNum = show; updateLineNumberAreaWidth(0); } } bool CSVWorld::ScriptEdit::isChangeLocked() const { return mChangeLocked != 0; } void CSVWorld::ScriptEdit::dragEnterEvent(QDragEnterEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) QPlainTextEdit::dragEnterEvent(event); else { setTextCursor(cursorForPosition(event->pos())); event->acceptProposedAction(); } } void CSVWorld::ScriptEdit::dragMoveEvent(QDragMoveEvent* event) { const CSMWorld::TableMimeData* mime = dynamic_cast(event->mimeData()); if (!mime) QPlainTextEdit::dragMoveEvent(event); else { setTextCursor(cursorForPosition(event->pos())); event->accept(); } } void CSVWorld::ScriptEdit::dropEvent(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 { QPlainTextEdit::dropEvent(event); return; } setTextCursor(cursorForPosition(event->pos())); if (mime->fromDocument(mDocument)) { std::vector records(mime->getData()); for (std::vector::iterator it = records.begin(); it != records.end(); ++it) { if (mAllowedTypes.contains(it->getType())) { if (stringNeedsQuote(it->getId())) { insertPlainText(QString::fromUtf8(('"' + it->getId() + '"').c_str())); } else { insertPlainText(QString::fromUtf8(it->getId().c_str())); } } } } } bool CSVWorld::ScriptEdit::stringNeedsQuote(const std::string& id) const { 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() { // Set tab width to specified number of characters using current font. setTabStopDistance(mTabCharCount * fontMetrics().horizontalAdvance(' ')); } void CSVWorld::ScriptEdit::wrapLines(bool wrap) { if (wrap) { setLineWrapMode(QPlainTextEdit::WidgetWidth); } else { setLineWrapMode(QPlainTextEdit::NoWrap); } } void CSVWorld::ScriptEdit::settingChanged(const CSMPrefs::Setting* setting) { // Determine which setting was changed. if (mHighlighter->settingChanged(setting)) { updateHighlighting(); } else if (*setting == "Scripts/mono-font") { setFont(setting->isTrue() ? mMonoFont : mDefaultFont); setTabWidth(); } else if (*setting == "Scripts/show-linenum") { showLineNum(setting->isTrue()); } else if (*setting == "Scripts/wrap-lines") { wrapLines(setting->isTrue()); } else if (*setting == "Scripts/tab-width") { mTabCharCount = setting->toInt(); setTabWidth(); } else if (*setting == "Scripts/highlight-occurrences") { mMarkOccurrences = setting->isTrue(); mHighlighter->setMarkedWord(""); updateHighlighting(); mHighlighter->setMarkOccurrences(mMarkOccurrences); } } void CSVWorld::ScriptEdit::idListChanged() { mHighlighter->invalidateIds(); if (!mUpdateTimer.isActive()) mUpdateTimer.start(0); } void CSVWorld::ScriptEdit::updateHighlighting() { if (isChangeLocked()) return; ChangeLock lock(*this); mHighlighter->rehighlight(); } int CSVWorld::ScriptEdit::lineNumberAreaWidth() { if (!mShowLineNum) return 0; int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * digits; return space; } void CSVWorld::ScriptEdit::updateLineNumberAreaWidth(int /* newBlockCount */) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } void CSVWorld::ScriptEdit::updateLineNumberArea(const QRect& rect, int dy) { if (dy) mLineNumberArea->scroll(0, dy); else mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height()); if (rect.contains(viewport()->rect())) updateLineNumberAreaWidth(0); } void CSVWorld::ScriptEdit::markOccurrences() { if (mMarkOccurrences) { QTextCursor cursor = textCursor(); // prevent infinite recursion with cursor.select(), // which ends up calling this function again // could be fixed with blockSignals, but mDocument is const disconnect(this, &ScriptEdit::cursorPositionChanged, this, nullptr); cursor.select(QTextCursor::WordUnderCursor); connect(this, &ScriptEdit::cursorPositionChanged, this, &ScriptEdit::markOccurrences); QString word = cursor.selectedText(); mHighlighter->setMarkedWord(word.toStdString()); mHighlighter->rehighlight(); } } void CSVWorld::ScriptEdit::commentSelection() { QTextCursor begin = textCursor(); QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.insertText(";"); } begin.endEditBlock(); } void CSVWorld::ScriptEdit::uncommentSelection() { QTextCursor begin = textCursor(); QTextCursor end = begin; begin.setPosition(begin.selectionStart()); begin.movePosition(QTextCursor::StartOfLine); end.setPosition(end.selectionEnd()); end.movePosition(QTextCursor::EndOfLine); begin.beginEditBlock(); for (; begin < end; begin.movePosition(QTextCursor::EndOfLine), begin.movePosition(QTextCursor::Right)) { begin.select(QTextCursor::LineUnderCursor); QString line = begin.selectedText(); if (line.size() == 0) continue; // get first nonspace character in line int index; for (index = 0; index != line.size(); ++index) { if (!line[index].isSpace()) break; } if (index != line.size() && line[index] == ';') { // remove the semicolon line.remove(index, 1); // put the line back begin.insertText(line); } } begin.endEditBlock(); } void CSVWorld::ScriptEdit::resizeEvent(QResizeEvent* e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); } void CSVWorld::ScriptEdit::contextMenuEvent(QContextMenuEvent* event) { QMenu* menu = createStandardContextMenu(); // remove redo/undo since they are disabled QList menuActions = menu->actions(); for (QList::iterator i = menuActions.begin(); i < menuActions.end(); ++i) { if ((*i)->text().contains("Undo") || (*i)->text().contains("Redo")) { (*i)->setVisible(false); } } menu->addAction(mCommentAction); menu->addAction(mUncommentAction); menu->exec(event->globalPos()); delete menu; } 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 startBlock = textCursor().blockNumber(); int endBlock = textCursor().blockNumber(); if (textCursor().hasSelection()) { QString str = textCursor().selection().toPlainText(); int offset = str.count("\n"); if (textCursor().position() < textCursor().anchor()) endBlock += offset; else startBlock -= offset; } 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()) { if (block.isVisible() && bottom >= event->rect().top()) { QFont newFont = painter.font(); QString number = QString::number(blockNumber + 1); if (blockNumber >= startBlock && blockNumber <= endBlock) { painter.setBackground(Qt::cyan); painter.setPen(Qt::darkMagenta); newFont.setBold(true); } else { painter.setBackground(background); painter.setPen(textColor); } painter.setFont(newFont); 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(); ++blockNumber; } } 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) { mScriptEdit->lineNumberAreaPaintEvent(event); } openmw-openmw-0.49.0/apps/opencs/view/world/scriptedit.hpp000066400000000000000000000065371503074453300236510ustar00rootroot00000000000000#ifndef SCRIPTEDIT_H #define SCRIPTEDIT_H #include #include #include #include #include #include #include #include "../../model/world/universalid.hpp" #include "scripthighlighter.hpp" 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 { class Document; } namespace CSVWorld { class LineNumberArea; /// \brief Editor for scripts. class ScriptEdit : public QPlainTextEdit { Q_OBJECT public: class ChangeLock { ScriptEdit& mEdit; 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: bool event(QEvent* event) override; public: ScriptEdit(const CSMDoc::Document& document, ScriptHighlighter::Mode mode, QWidget* parent); /// 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; void lineNumberAreaPaintEvent(QPaintEvent* event); int lineNumberAreaWidth(); void showLineNum(bool show); protected: void resizeEvent(QResizeEvent* e) override; void contextMenuEvent(QContextMenuEvent* event) override; private: QVector mAllowedTypes; const CSMDoc::Document& mDocument; const QRegularExpression mWhiteListQuotes; void dragEnterEvent(QDragEnterEvent* event) override; void dropEvent(QDropEvent* event) override; void dragMoveEvent(QDragMoveEvent* event) override; bool stringNeedsQuote(const std::string& id) const; /// \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); private slots: /// \brief Update editor when related setting is changed. /// \param setting Setting that was changed. void settingChanged(const CSMPrefs::Setting* setting); void idListChanged(); void updateHighlighting(); void updateLineNumberAreaWidth(int newBlockCount); void updateLineNumberArea(const QRect&, int); void markOccurrences(); void commentSelection(); void uncommentSelection(); }; class LineNumberArea : public QWidget { ScriptEdit* mScriptEdit; public: LineNumberArea(ScriptEdit* editor); QSize sizeHint() const override; protected: void paintEvent(QPaintEvent* event) override; }; } #endif // SCRIPTEDIT_H openmw-openmw-0.49.0/apps/opencs/view/world/scripterrortable.cpp000066400000000000000000000121531503074453300250470ustar00rootroot00000000000000#include "scripterrortable.hpp" #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) { 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()); } void CSVWorld::ScriptErrorTable::report(const std::string& message, Type type) { 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) { int row = rowCount(); 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); 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); } else { 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); } 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); } CSVWorld::ScriptErrorTable::ScriptErrorTable(const CSMDoc::Document& document, QWidget* parent) : QTableWidget(parent) , mContext(document.getData()) { setColumnCount(4); QStringList headers; headers << "Severity" << "Line" << "Description"; setHorizontalHeaderLabels(headers); horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); horizontalHeader()->setStretchLastSection(true); verticalHeader()->hide(); setColumnHidden(3, true); setSelectionMode(QAbstractItemView::NoSelection); Compiler::registerExtensions(mExtensions); mContext.setExtensions(&mExtensions); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptErrorTable::settingChanged); CSMPrefs::get()["Scripts"].update(); connect(this, &QTableWidget::cellClicked, this, &ScriptErrorTable::cellClicked); } void CSVWorld::ScriptErrorTable::update(const std::string& source) { clear(); try { std::istringstream input(source); Compiler::Scanner scanner(*this, input, mContext.getExtensions()); Compiler::FileParser parser(*this, mContext); scanner.scan(parser); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { addMessage(error.what(), CSMDoc::Message::Severity_SeriousError); } } void CSVWorld::ScriptErrorTable::clear() { setRowCount(0); } bool CSVWorld::ScriptErrorTable::clearLocals(const std::string& script) { return mContext.clearLocals(script); } void CSVWorld::ScriptErrorTable::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "Scripts/warnings") setWarningsMode(setting->toString()); } void CSVWorld::ScriptErrorTable::cellClicked(int row, int column) { 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); } } openmw-openmw-0.49.0/apps/opencs/view/world/scripterrortable.hpp000066400000000000000000000031531503074453300250540ustar00rootroot00000000000000#ifndef CSV_WORLD_SCRIPTERRORTABLE_H #define CSV_WORLD_SCRIPTERRORTABLE_H #include #include #include #include #include "../../model/doc/messages.hpp" #include "../../model/world/scriptcontext.hpp" namespace Compiler { struct TokenLoc; } namespace CSMDoc { class Document; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class ScriptErrorTable : public QTableWidget, private Compiler::ErrorHandler { Q_OBJECT Compiler::Extensions mExtensions; CSMWorld::ScriptContext mContext; void report(const std::string& message, const Compiler::TokenLoc& loc, Type type) override; ///< Report error to the user. void report(const std::string& message, Type type) override; ///< Report a file related error void addMessage(const std::string& message, CSMDoc::Message::Severity severity, int line = -1, int column = -1); void setWarningsMode(const std::string& value); public: ScriptErrorTable(const CSMDoc::Document& document, QWidget* parent = nullptr); void update(const std::string& source); void clear(); /// Clear local variable cache for \a script. /// /// \return Were there any locals that needed clearing? bool clearLocals(const std::string& script); private slots: void settingChanged(const CSMPrefs::Setting* setting); void cellClicked(int row, int column); signals: void highlightError(int line, int column); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/scripthighlighter.cpp000066400000000000000000000115441503074453300252070ustar00rootroot00000000000000#include "scripthighlighter.hpp" #include #include #include #include #include #include #include #include #include class QTextDocument; namespace CSMWorld { 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) { highlight(loc, Type_Float); return true; } bool CSVWorld::ScriptHighlighter::parseName( const std::string& name, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { 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) { 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) { highlight(loc, Type_Special); return true; } bool CSVWorld::ScriptHighlighter::parseComment( const std::string& comment, const Compiler::TokenLoc& loc, Compiler::Scanner& scanner) { highlight(loc, Type_Comment); return true; } void CSVWorld::ScriptHighlighter::parseEOF(Compiler::Scanner& scanner) {} 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; int index = loc.mColumn; // compensate for bug in Compiler::Scanner (position of token is the character after the token) index -= length; QTextCharFormat scheme = mScheme[type]; if (mMarkOccurrences && type == Type_Name && loc.mLiteral == mMarkedWord) scheme.merge(mScheme[Type_Highlight]); 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) { QColor color("black"); QTextCharFormat format; format.setForeground(color); 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); } void CSVWorld::ScriptHighlighter::highlightBlock(const QString& text) { std::istringstream stream(text.toUtf8().constData()); Compiler::Scanner scanner(mErrorHandler, stream, mContext.getExtensions()); try { scanner.scan(*this); } catch (...) { } // ignore syntax errors } void CSVWorld::ScriptHighlighter::setMarkOccurrences(bool flag) { mMarkOccurrences = flag; } void CSVWorld::ScriptHighlighter::setMarkedWord(const std::string& name) { mMarkedWord = name; } void CSVWorld::ScriptHighlighter::invalidateIds() { mContext.invalidateIds(); } bool CSVWorld::ScriptHighlighter::settingChanged(const CSMPrefs::Setting* setting) { 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]) { QTextCharFormat format; if (i == Type_Highlight) format.setBackground(setting->toColor()); else format.setForeground(setting->toColor()); mScheme[static_cast(i)] = format; return true; } } return false; } openmw-openmw-0.49.0/apps/opencs/view/world/scripthighlighter.hpp000066400000000000000000000057141503074453300252160ustar00rootroot00000000000000#ifndef CSV_WORLD_SCRIPTHIGHLIGHTER_H #define CSV_WORLD_SCRIPTHIGHLIGHTER_H #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; } 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/scriptsubview.cpp000066400000000000000000000255161503074453300244010ustar00rootroot00000000000000#include "scriptsubview.hpp" #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/world/commands.hpp" #include "../../model/world/data.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/universalid.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); mLayout.insertWidget(1, mButtons); connect(mButtons, &RecordButtonBar::switchToRow, this, &ScriptSubView::switchToRow); connect(this, &ScriptSubView::universalIdChanged, mButtons, &RecordButtonBar::universalIdChanged); } void CSVWorld::ScriptSubView::recompile() { if (!mCompileDelay->isActive() && !isDeleted()) 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; } void CSVWorld::ScriptSubView::updateDeletedState() { if (isDeleted()) { mErrors->clear(); adjustSplitter(); mEditor->setEnabled(false); } else { mEditor->setEnabled(true); recompile(); } } void CSVWorld::ScriptSubView::adjustSplitter() { QList sizes; if (mErrors->rowCount()) { if (mErrors->height()) return; // keep old height if the error panel was already open sizes << (mMain->height() - mErrorHeight - mMain->handleWidth()) << mErrorHeight; } else { if (mErrors->height()) mErrorHeight = mErrors->height(); sizes << 1 << 0; } 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()) { std::vector selection(1, id.getId()); mCommandDispatcher.setSelection(selection); 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); mErrors = new ScriptErrorTable(document, this); mMain->addWidget(mErrors); QList sizes; sizes << 1 << 0; mMain->setSizes(sizes); QWidget* widget = new QWidget(this); widget->setLayout(&mLayout); setWidget(widget); 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); QString source = mModel->data(mModel->getModelIndex(id.getId(), mColumn)).toString(); mEditor->setPlainText(source); // bottom box and buttons mBottom = new TableBottomBox(CreatorFactory(), document, id, this); connect(mBottom, &TableBottomBox::requestFocus, this, &ScriptSubView::switchToId); mLayout.addWidget(mBottom); // signals connect(mEditor, &ScriptEdit::textChanged, this, &ScriptSubView::textChanged); connect(mModel, &CSMWorld::IdTable::dataChanged, this, &ScriptSubView::dataChanged); connect(mModel, &CSMWorld::IdTable::rowsAboutToBeRemoved, this, &ScriptSubView::rowsAboutToBeRemoved); updateStatusBar(); connect(mEditor, &ScriptEdit::cursorPositionChanged, this, &ScriptSubView::updateStatusBar); mErrors->update(source.toUtf8().constData()); connect(mErrors, &ScriptErrorTable::highlightError, this, &ScriptSubView::highlightError); mCompileDelay = new QTimer(this); mCompileDelay->setSingleShot(true); connect(mCompileDelay, &QTimer::timeout, this, &ScriptSubView::updateRequest); updateDeletedState(); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &ScriptSubView::settingChanged); CSMPrefs::get()["Scripts"].update(); } void CSVWorld::ScriptSubView::setStatusBar(bool show) { mBottom->setStatusBar(show); } void CSVWorld::ScriptSubView::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "Scripts/toolbar") { if (setting->isTrue()) { addButtonBar(); } else if (mButtons) { mLayout.removeWidget(mButtons); delete mButtons; mButtons = nullptr; } } else if (*setting == "Scripts/compile-delay") { mCompileDelay->setInterval(setting->toInt()); } else if (*setting == "Scripts/warnings") recompile(); } void CSVWorld::ScriptSubView::updateStatusBar() { mBottom->positionChanged(mEditor->textCursor().blockNumber() + 1, mEditor->textCursor().columnNumber() + 1); } void CSVWorld::ScriptSubView::setEditLock(bool locked) { mEditor->setReadOnly(locked); if (mButtons) mButtons->setEditLock(locked); mCommandDispatcher.setEditLock(locked); } void CSVWorld::ScriptSubView::useHint(const std::string& hint) { if (hint.empty()) return; int line = 0, column = 0; char c; 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(); 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"; pos = stringSize; } for (int i = 0; i <= pos; ++i) { if (source[i] == '\n') { ++line; column = i + 1; } } column = pos - column; break; } case 'l': if (!(stream >> c >> line >> column)) return; } QTextCursor cursor = mEditor->textCursor(); cursor.movePosition(QTextCursor::Start); if (cursor.movePosition(QTextCursor::Down, QTextCursor::MoveAnchor, line)) cursor.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column); mEditor->setFocus(); mEditor->setTextCursor(cursor); } void CSVWorld::ScriptSubView::textChanged() { if (mEditor->isChangeLocked()) return; ScriptEdit::ChangeLock lock(*mEditor); QString source = mEditor->toPlainText(); mDocument.getUndoStack().push( new CSMWorld::ModifyCommand(*mModel, mModel->getModelIndex(getUniversalId().getId(), mColumn), source)); recompile(); } void CSVWorld::ScriptSubView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight) { if (mEditor->isChangeLocked()) return; ScriptEdit::ChangeLock lock(*mEditor); bool updateRequired = false; 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)) updateRequired = true; } QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); if (index.row() >= topLeft.row() && index.row() <= bottomRight.row()) { if (mStateColumn >= topLeft.column() && mStateColumn <= bottomRight.column()) updateDeletedState(); if (mColumn >= topLeft.column() && mColumn <= bottomRight.column()) { QString source = mModel->data(index).toString(); QTextCursor cursor = mEditor->textCursor(); mEditor->setPlainText(source); mEditor->setTextCursor(cursor); updateRequired = true; } } if (updateRequired) recompile(); } void CSVWorld::ScriptSubView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { bool updateRequired = false; for (int i = start; i <= end; ++i) { 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); if (!parent.isValid() && index.row() >= start && index.row() <= end) emit closeRequest(); } 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)); 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); updateDeletedState(); } void CSVWorld::ScriptSubView::switchToId(const std::string& id) { switchToRow(mModel->getModelIndex(id, 0).row()); } 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); mEditor->setFocus(); mEditor->setTextCursor(cursor); } void CSVWorld::ScriptSubView::updateRequest() { QModelIndex index = mModel->getModelIndex(getUniversalId().getId(), mColumn); QString source = mModel->data(index).toString(); mErrors->update(source.toUtf8().constData()); adjustSplitter(); } openmw-openmw-0.49.0/apps/opencs/view/world/scriptsubview.hpp000066400000000000000000000037471503074453300244100ustar00rootroot00000000000000#ifndef CSV_WORLD_SCRIPTSUBVIEW_H #define CSV_WORLD_SCRIPTSUBVIEW_H #include #include #include #include "../../model/world/commanddispatcher.hpp" #include "../doc/subview.hpp" class QModelIndex; class QObject; class QSplitter; class QTimer; namespace CSMDoc { class Document; } namespace CSMWorld { class IdTable; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class ScriptEdit; class RecordButtonBar; class TableBottomBox; class ScriptErrorTable; 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: void addButtonBar(); void recompile(); bool isDeleted() const; void updateDeletedState(); void adjustSplitter(); public: ScriptSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document); void setEditLock(bool locked) override; void useHint(const std::string& hint) override; void setStatusBar(bool show) override; public slots: void textChanged(); void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end); private slots: void settingChanged(const CSMPrefs::Setting* setting); void updateStatusBar(); void switchToRow(int row); void switchToId(const std::string& id); void highlightError(int line, int column); void updateRequest(); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/startscriptcreator.cpp000066400000000000000000000063661503074453300254340ustar00rootroot00000000000000#include "startscriptcreator.hpp" #include #include #include #include #include #include "../../model/doc/document.hpp" #include "../../model/world/columns.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(); } CSMWorld::IdTable& CSVWorld::StartScriptCreator::getStartScriptsTable() const { 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) { setManualEditing(false); // Add script ID input label. QLabel* label = new QLabel("Script", this); insertBeforeButtons(label, false); // Add script ID input with auto-completion. // Only existing script IDs are accepted so no ID validation is performed. CSMWorld::ColumnBase::Display displayType = CSMWorld::ColumnBase::Display_Script; mScript = new CSVWidget::DropLineEdit(displayType, this); mScript->setCompleter(completionManager.getCompleter(displayType).get()); insertBeforeButtons(mScript, true); 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) { CSVWorld::GenericCreator::cloneMode(originId, type); // Look up cloned record in start scripts table and set script ID text. CSMWorld::IdTable& table = getStartScriptsTable(); int column = table.findColumnIndex(CSMWorld::Columns::ColumnId_Id); mScript->setText(table.data(table.getModelIndex(originId, column)).toString()); } std::string CSVWorld::StartScriptCreator::getErrors() const { const ESM::RefId scriptId = ESM::RefId::stringRefId(getId()); // Check user input for any errors. std::string errors; if (scriptId.empty()) { errors = "No Script ID entered"; } else if (getData().getScripts().searchId(scriptId) == -1) { errors = "Script ID not found"; } else if (getData().getStartScripts().searchId(scriptId) > -1) { errors = "Script with this ID already registered as Start Script"; } return errors; } void CSVWorld::StartScriptCreator::focus() { mScript->setFocus(); } void CSVWorld::StartScriptCreator::reset() { CSVWorld::GenericCreator::reset(); mScript->setText(""); } void CSVWorld::StartScriptCreator::scriptChanged() { update(); } CSVWorld::Creator* CSVWorld::StartScriptCreatorFactory::makeCreator( CSMDoc::Document& document, const CSMWorld::UniversalId& id) const { return new StartScriptCreator(document.getData(), document.getUndoStack(), id, document.getIdCompletionManager()); } openmw-openmw-0.49.0/apps/opencs/view/world/startscriptcreator.hpp000066400000000000000000000037121503074453300254310ustar00rootroot00000000000000#ifndef STARTSCRIPTCREATOR_HPP #define STARTSCRIPTCREATOR_HPP #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; } namespace CSVWorld { /// \brief Record creator for start scripts. class StartScriptCreator : public GenericCreator { Q_OBJECT CSVWidget::DropLineEdit* mScript; private: /// \return script ID entered by user. std::string getId() const override; /// \return reference to table containing start scripts. CSMWorld::IdTable& getStartScriptsTable() const; 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; /// \return Error description for current user input. std::string getErrors() const override; /// \brief Set focus to script ID input widget. void focus() override; /// \brief Clear script ID input widget. void reset() override; private slots: /// \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; }; } #endif // STARTSCRIPTCREATOR_HPP openmw-openmw-0.49.0/apps/opencs/view/world/subviews.cpp000066400000000000000000000231231503074453300233270ustar00rootroot00000000000000#include "subviews.hpp" #include "../doc/subviewfactoryimp.hpp" #include "bodypartcreator.hpp" #include "cellcreator.hpp" #include "dialoguecreator.hpp" #include "dialoguesubview.hpp" #include "genericcreator.hpp" #include "globalcreator.hpp" #include "infocreator.hpp" #include "landcreator.hpp" #include "pathgridcreator.hpp" #include "previewsubview.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) { // 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_Skills, new CSVDoc::SubViewFactoryWithCreator); manager.add(CSMWorld::UniversalId::Type_MagicEffects, new CSVDoc::SubViewFactoryWithCreator); static const CSMWorld::UniversalId::Type sTableTypes[] = { CSMWorld::UniversalId::Type_Classes, CSMWorld::UniversalId::Type_Factions, CSMWorld::UniversalId::Type_Races, CSMWorld::UniversalId::Type_Sounds, CSMWorld::UniversalId::Type_Regions, CSMWorld::UniversalId::Type_Birthsigns, CSMWorld::UniversalId::Type_Spells, CSMWorld::UniversalId::Type_Enchantments, CSMWorld::UniversalId::Type_SoundGens, // end marker CSMWorld::UniversalId::Type_None, }; 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_StartScripts, 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_References, new CSVDoc::SubViewFactoryWithCreator); manager.add( CSMWorld::UniversalId::Type_Topics, new CSVDoc::SubViewFactoryWithCreator); manager.add(CSMWorld::UniversalId::Type_Journals, new CSVDoc::SubViewFactoryWithCreator); manager.add(CSMWorld::UniversalId::Type_TopicInfos, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add(CSMWorld::UniversalId::Type_JournalInfos, new CSVDoc::SubViewFactoryWithCreator(false)); manager.add(CSMWorld::UniversalId::Type_Pathgrids, 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_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); // Subviews for editing/viewing individual records 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_Scene, new CSVDoc::SubViewFactory); // More other stuff manager.add(CSMWorld::UniversalId::Type_Filters, new CSVDoc::SubViewFactoryWithCreator>); manager.add(CSMWorld::UniversalId::Type_DebugProfiles, new CSVDoc::SubViewFactoryWithCreator>); manager.add(CSMWorld::UniversalId::Type_Scripts, new CSVDoc::SubViewFactoryWithCreator>); // Dialogue subviews static const CSMWorld::UniversalId::Type sTableTypes2[] = { CSMWorld::UniversalId::Type_Region, CSMWorld::UniversalId::Type_Spell, CSMWorld::UniversalId::Type_Birthsign, CSMWorld::UniversalId::Type_Global, CSMWorld::UniversalId::Type_Race, CSMWorld::UniversalId::Type_Class, CSMWorld::UniversalId::Type_Sound, CSMWorld::UniversalId::Type_Faction, CSMWorld::UniversalId::Type_Enchantment, CSMWorld::UniversalId::Type_SoundGen, // 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)); manager.add(CSMWorld::UniversalId::Type_BodyPart, new CSVDoc::SubViewFactoryWithCreator>(false)); 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_MagicEffect, 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_Reference, 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_TopicInfo, 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_Pathgrid, 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_DebugProfile, new CSVDoc::SubViewFactoryWithCreator>(false)); manager.add(CSMWorld::UniversalId::Type_Filter, new CSVDoc::SubViewFactoryWithCreator>(false)); manager.add(CSMWorld::UniversalId::Type_MetaData, new CSVDoc::SubViewFactory); // preview manager.add(CSMWorld::UniversalId::Type_Preview, new CSVDoc::SubViewFactory); } openmw-openmw-0.49.0/apps/opencs/view/world/subviews.hpp000066400000000000000000000003271503074453300233350ustar00rootroot00000000000000#ifndef CSV_WORLD_SUBVIEWS_H #define CSV_WORLD_SUBVIEWS_H namespace CSVDoc { class SubViewFactoryManager; } namespace CSVWorld { void addSubViewFactories(CSVDoc::SubViewFactoryManager& manager); } #endif openmw-openmw-0.49.0/apps/opencs/view/world/table.cpp000066400000000000000000001000661503074453300225510ustar00rootroot00000000000000#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 "../../model/doc/document.hpp" #include "../../model/world/commanddispatcher.hpp" #include "../../model/world/commands.hpp" #include "../../model/world/idtable.hpp" #include "../../model/world/idtablebase.hpp" #include "../../model/world/infotableproxymodel.hpp" #include "../../model/world/landtexturetableproxymodel.hpp" #include "../../model/prefs/shortcut.hpp" #include "../../model/prefs/state.hpp" #include "tableeditidaction.hpp" #include "tableheadermouseeventhandler.hpp" #include "util.hpp" namespace CSMFilter { class Node; } void CSVWorld::Table::contextMenuEvent(QContextMenuEvent* event) { // configure dispatcher mDispatcher->setSelection(getSelectedIds()); std::vector extendedTypes = mDispatcher->getExtendedTypes(); mDispatcher->setExtendedTypes(extendedTypes); // create context menu QModelIndexList selectedRows = selectionModel()->selectedRows(); QMenu menu(this); /// \todo add menu items for select all and clear selection int currentRow = rowAt(event->y()); int currentColumn = columnAt(event->x()); if (mEditIdAction->isValidIdCell(currentRow, currentColumn)) { mEditIdAction->setCell(currentRow, currentColumn); menu.addAction(mEditIdAction); menu.addSeparator(); } if (!mEditLock && !(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { if (selectedRows.size() == 1) { menu.addAction(mEditAction); if (mCreateAction) menu.addAction(mCloneAction); } if (mTouchAction) menu.addAction(mTouchAction); if (mCreateAction) menu.addAction(mCreateAction); if (mDispatcher->canRevert()) { menu.addAction(mRevertAction); if (!extendedTypes.empty()) menu.addAction(mExtendedRevertAction); } if (mDispatcher->canDelete()) { menu.addAction(mDeleteAction); if (!extendedTypes.empty()) menu.addAction(mExtendedDeleteAction); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_ReorderWithinTopic) { /// \todo allow reordering of multiple rows if (selectedRows.size() == 1) { int column = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Topic); if (column == -1) column = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Journal); if (column != -1) { int row = mProxyModel->mapToSource(mProxyModel->index(selectedRows.begin()->row(), 0)).row(); QString curData = mModel->data(mModel->index(row, column)).toString(); if (row > 0) { QString prevData = mModel->data(mModel->index(row - 1, column)).toString(); if (Misc::StringUtils::ciEqual(curData.toStdString(), prevData.toStdString())) { menu.addAction(mMoveUpAction); } } if (row < mModel->rowCount() - 1) { QString nextData = mModel->data(mModel->index(row + 1, column)).toString(); if (Misc::StringUtils::ciEqual(curData.toStdString(), nextData.toStdString())) { menu.addAction(mMoveDownAction); } } } } } } if (selectedRows.size() == 1) { int row = selectedRows.begin()->row(); row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View) { CSMWorld::UniversalId id = mModel->view(row).first; 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 (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Preview) { const CSMWorld::UniversalId id = getUniversalId(currentRow); const CSMWorld::UniversalId::Type type = id.getType(); QModelIndex index = mModel->index(row, mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Modification)); 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 (mHelpAction) menu.addAction(mHelpAction); menu.exec(event->globalPos()); } void CSVWorld::Table::mouseDoubleClickEvent(QMouseEvent* event) { Qt::KeyboardModifiers modifiers = event->modifiers() & (Qt::ShiftModifier | Qt::ControlModifier); QModelIndex index = currentIndex(); selectionModel()->select( index, QItemSelectionModel::Clear | QItemSelectionModel::Select | QItemSelectionModel::Rows); std::map::iterator iter = mDoubleClickActions.find(modifiers); if (iter == mDoubleClickActions.end()) { event->accept(); return; } switch (iter->second) { case Action_None: event->accept(); break; case Action_InPlaceEdit: DragRecordTable::mouseDoubleClickEvent(event); break; case Action_EditRecord: event->accept(); editRecord(); break; case Action_View: event->accept(); viewRecord(); break; case Action_Revert: event->accept(); if (mDispatcher->canRevert()) mDispatcher->executeRevert(); break; case Action_Delete: event->accept(); if (mDispatcher->canDelete()) mDispatcher->executeDelete(); break; case Action_EditRecordAndClose: event->accept(); editRecord(); emit closeRequest(); break; case Action_ViewAndClose: event->accept(); viewRecord(); emit closeRequest(); break; } } 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)); 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::createNewInfoRecord, this, &CSVWorld::Table::createRecordsDirectlyRequest); } else if (isLtexTable) { mProxyModel = new CSMWorld::LandTextureTableProxyModel(this); } else { mProxyModel = new CSMWorld::IdTableProxyModel(this); } mProxyModel->setSourceModel(mModel); mDispatcher = new CSMWorld::CommandDispatcher(document, id, this); 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); int columns = mModel->columnCount(); for (int i = 0; i < columns; ++i) { 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()); CommandDelegate* delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, mDispatcher, document, this); mDelegates.push_back(delegate); setItemDelegateForColumn(i, delegate); } else 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, &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, &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, &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); } if (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) { mTouchAction = new QAction(tr("Touch Record"), this); 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, &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, &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, &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, &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, &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, &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, &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, &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, &QAction::triggered, this, &Table::editCell); addAction(mEditIdAction); 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, &CSMWorld::IdTableProxyModel::rowsRemoved, this, &Table::tableSizeUpdate); 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, &CSMWorld::IdTableProxyModel::dataChanged, this, &Table::dataChangedEvent); 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)); connect(&CSMPrefs::State::get(), &CSMPrefs::State::settingChanged, this, &Table::settingChanged); CSMPrefs::get()["ID Tables"].update(); new TableHeaderMouseEventHandler(this); } void CSVWorld::Table::setEditLock(bool locked) { for (std::vector::iterator iter(mDelegates.begin()); iter != mDelegates.end(); ++iter) (*iter)->setEditLock(locked); DragRecordTable::setEditLock(locked); mDispatcher->setEditLock(locked); } CSMWorld::UniversalId CSVWorld::Table::getUniversalId(int row) const { row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); 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()); } std::vector CSVWorld::Table::getSelectedIds() const { std::vector ids; QModelIndexList selectedRows = selectionModel()->selectedRows(); int columnIndex = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); 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()); } return ids; } void CSVWorld::Table::editRecord() { if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size() == 1) emit editRequest(getUniversalId(selectedRows.begin()->row()), ""); } } void CSVWorld::Table::cloneRecord() { if (!mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) { QModelIndexList selectedRows = selectionModel()->selectedRows(); const CSMWorld::UniversalId& toClone = getUniversalId(selectedRows.begin()->row()); if (selectedRows.size() == 1) { emit cloneRequest(toClone); } } } void CSVWorld::Table::touchRecord() { if (!mEditLock && mModel->getFeatures() & CSMWorld::IdTableBase::Feature_AllowTouch) { std::vector touchIds; QModelIndexList selectedRows = selectionModel()->selectedRows(); for (auto it = selectedRows.begin(); it != selectedRows.end(); ++it) { touchIds.push_back(getUniversalId(it->row())); } emit touchRequest(touchIds); } } void CSVWorld::Table::moveUpRecord() { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size() == 1) { int row2 = selectedRows.begin()->row(); if (row2 > 0) { int row = row2 - 1; 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"); std::vector 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)); } } } void CSVWorld::Table::moveDownRecord() { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size() == 1) { int row = selectedRows.begin()->row(); if (row < mProxyModel->rowCount() - 1) { int row2 = row + 1; 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"); std::vector 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)); } } } void CSVWorld::Table::moveRecords(QDropEvent* event) { if (mEditLock || (mModel->getFeatures() & CSMWorld::IdTableBase::Feature_Constant)) return; QModelIndex targedIndex = indexAt(event->pos()); QModelIndexList selectedRows = selectionModel()->selectedRows(); int targetRowRaw = targedIndex.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 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; } std::vector newOrder(highestDifference + 1); for (int i = 0; i <= highestDifference; ++i) { newOrder[i] = i; } if (selectedRows.size() > 1) { Log(Debug::Warning) << "Move operation failed: Moving multiple selections isn't implemented."; return; } for (const auto& thisRowData : selectedRows) { /* Moving algorithm description a) Remove the (ORIGIN + 1)th list member. b) Add (ORIGIN+1)th list member with value TARGET c) If ORIGIN > TARGET,d_INC; ELSE d_DEC d_INC) increase all members after (and including) the TARGET by one, stop before hitting ORIGINth address d_DEC) decrease all members after the ORIGIN by one, stop after hitting address TARGET */ int originRowRaw = thisRowData.row(); int originRow = mProxyModel->mapToSource(mProxyModel->index(originRowRaw, 0)).row(); newOrder.erase(newOrder.begin() + originRow - baseRow - 1); newOrder.emplace(newOrder.begin() + originRow - baseRow - 1, targetRow - baseRow - 1); if (originRow > targetRow) { for (int i = targetRow - baseRow - 1; i < originRow - baseRow - 1; ++i) { ++newOrder[i]; } } else { for (int i = originRow - baseRow; i <= targetRow - baseRow - 1; ++i) { --newOrder[i]; } } } mDocument.getUndoStack().push( new CSMWorld::ReorderRowsCommand(dynamic_cast(*mModel), baseRow + 1, newOrder)); } void CSVWorld::Table::editCell() { emit editRequest(mEditIdAction->getCurrentId(), ""); } void CSVWorld::Table::openHelp() { Misc::HelpViewer::openHelp("manuals/openmw-cs/tables.html"); } void CSVWorld::Table::viewRecord() { if (!(mModel->getFeatures() & CSMWorld::IdTableBase::Feature_View)) return; QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size() == 1) { int row = selectedRows.begin()->row(); row = mProxyModel->mapToSource(mProxyModel->index(row, 0)).row(); std::pair params = mModel->view(row); if (params.first.getType() != CSMWorld::UniversalId::Type_None) emit editRequest(params.first, params.second); } } void CSVWorld::Table::previewRecord() { QModelIndexList selectedRows = selectionModel()->selectedRows(); if (selectedRows.size() == 1) { CSMWorld::UniversalId id = getUniversalId(selectedRows.begin()->row()); 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), ""); } } void CSVWorld::Table::executeExtendedDelete() { if (CSMPrefs::get()["ID Tables"]["extended-config"].isTrue()) { emit extendedDeleteConfigRequest(getSelectedIds()); } else { QMetaObject::invokeMethod(mDispatcher, "executeExtendedDelete", Qt::QueuedConnection); } } void CSVWorld::Table::executeExtendedRevert() { if (CSMPrefs::get()["ID Tables"]["extended-config"].isTrue()) { emit extendedRevertConfigRequest(getSelectedIds()); } else { QMetaObject::invokeMethod(mDispatcher, "executeExtendedRevert", Qt::QueuedConnection); } } void CSVWorld::Table::settingChanged(const CSMPrefs::Setting* setting) { if (*setting == "ID Tables/jump-to-added") { if (setting->toString() == "Jump and Select") { mJumpToAddedRecord = true; mUnselectAfterJump = false; } else if (setting->toString() == "Jump Only") { mJumpToAddedRecord = true; mUnselectAfterJump = true; } else // No Jump { mJumpToAddedRecord = false; mUnselectAfterJump = false; } } else if (*setting == "Records/type-format" || *setting == "Records/status-format") { int columns = mModel->columnCount(); for (int i = 0; i < columns; ++i) if (QAbstractItemDelegate* delegate = itemDelegateForColumn(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") { std::string modifierString = setting->getKey().substr(6); Qt::KeyboardModifiers modifiers; if (modifierString == "-s") modifiers = Qt::ShiftModifier; else if (modifierString == "-c") modifiers = Qt::ControlModifier; else if (modifierString == "-sc") modifiers = Qt::ShiftModifier | Qt::ControlModifier; DoubleClickAction action = Action_None; std::string value = setting->toString(); if (value == "Edit in Place") action = Action_InPlaceEdit; else if (value == "Edit Record") action = Action_EditRecord; else if (value == "View") action = Action_View; else if (value == "Revert") action = Action_Revert; else if (value == "Delete") action = Action_Delete; else if (value == "Edit Record and Close") action = Action_EditRecordAndClose; else if (value == "View and Close") action = Action_ViewAndClose; mDoubleClickActions[modifiers] = action; } } void CSVWorld::Table::tableSizeUpdate() { int size = 0; int deleted = 0; int modified = 0; if (mProxyModel->columnCount() > 0) { int rows = mProxyModel->rowCount(); int columnIndex = mModel->searchColumnIndex(CSMWorld::Columns::ColumnId_Modification); if (columnIndex != -1) { for (int i = 0; i < rows; ++i) { QModelIndex index = mProxyModel->mapToSource(mProxyModel->index(i, 0)); 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; } } } else size = rows; } emit tableSizeChanged(size, deleted, modified); } void CSVWorld::Table::selectionSizeUpdate() { emit selectionSizeChanged(selectionModel()->selectedRows().size()); } void CSVWorld::Table::requestFocus(const std::string& id) { QModelIndex index = mProxyModel->getModelIndex(id, 0); if (index.isValid()) { // This will scroll to the row. selectRow(index.row()); // This will actually select it. selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } void CSVWorld::Table::recordFilterChanged(std::shared_ptr filter) { mProxyModel->setFilter(filter); tableSizeUpdate(); selectionSizeUpdate(); } void CSVWorld::Table::mouseMoveEvent(QMouseEvent* event) { if (event->buttons() & Qt::LeftButton) { startDragFromTable(*this, indexAt(event->pos())); } } std::vector CSVWorld::Table::getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const { const int count = mModel->columnCount(); 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()); if (display == columndisplay) { titles.emplace_back(mModel->headerData(i, Qt::Horizontal).toString().toUtf8().constData()); } } return titles; } std::vector CSVWorld::Table::getDraggedRecords() const { QModelIndexList selectedRows = selectionModel()->selectedRows(); std::vector idToDrag; for (QModelIndex& it : selectedRows) idToDrag.push_back(getUniversalId(it.row())); return idToDrag; } // parent, start and end depend on the model sending the signal, in this case mProxyModel // // If, for example, mModel was used instead, then scrolTo() should use the index // mProxyModel->mapFromSource(mModel->index(end, 0)) void CSVWorld::Table::rowAdded(const std::string& id) { tableSizeUpdate(); if (mJumpToAddedRecord) { int idColumn = mModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); int end = mProxyModel->getModelIndex(id, idColumn).row(); selectRow(end); // without this delay the scroll works but goes to top for add/clone QMetaObject::invokeMethod(this, "queuedScrollTo", Qt::QueuedConnection, Q_ARG(int, end)); if (mUnselectAfterJump) clearSelection(); } } void CSVWorld::Table::queuedScrollTo(int row) { scrollTo(mProxyModel->index(row, 0), QAbstractItemView::PositionAtCenter); } void CSVWorld::Table::dataChangedEvent(const QModelIndex& topLeft, const QModelIndex& bottomRight) { tableSizeUpdate(); if (mAutoJump) { selectRow(bottomRight.row()); scrollTo(bottomRight, QAbstractItemView::PositionAtCenter); } } void CSVWorld::Table::jumpAfterModChanged(int state) { if (state == Qt::Checked) mAutoJump = true; else mAutoJump = false; } openmw-openmw-0.49.0/apps/opencs/view/world/table.hpp000066400000000000000000000106511503074453300225560ustar00rootroot00000000000000#ifndef CSV_WORLD_TABLE_H #define CSV_WORLD_TABLE_H #include #include #include #include #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 { class Document; } namespace CSMWorld { class IdTableProxyModel; class IdTableBase; class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { class CommandDelegate; class TableEditIdAction; ///< Table widget class Table : public DragRecordTable { Q_OBJECT 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; private: void contextMenuEvent(QContextMenuEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; protected: void mouseDoubleClickEvent(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. virtual void setEditLock(bool locked); CSMWorld::UniversalId getUniversalId(int row) const; std::vector getColumnsWithDisplay(CSMWorld::ColumnBase::Display display) const; std::vector getSelectedIds() const; std::vector getDraggedRecords() const override; signals: void editRequest(const CSMWorld::UniversalId& id, const std::string& hint); 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 createRequest(); void createRecordsDirectlyRequest(const std::string& id); void cloneRequest(const CSMWorld::UniversalId&); void touchRequest(const std::vector& ids); void closeRequest(); void extendedDeleteConfigRequest(const std::vector& selectedIds); void extendedRevertConfigRequest(const std::vector& selectedIds); private slots: void editCell(); static void openHelp(); void editRecord(); void cloneRecord(); void touchRecord(); void moveUpRecord(); void moveDownRecord(); void moveRecords(QDropEvent* event); void viewRecord(); void previewRecord(); void executeExtendedDelete(); void executeExtendedRevert(); public slots: void settingChanged(const CSMPrefs::Setting* setting); void tableSizeUpdate(); void selectionSizeUpdate(); void requestFocus(const std::string& id); void recordFilterChanged(std::shared_ptr filter); void rowAdded(const std::string& id); void dataChangedEvent(const QModelIndex& topLeft, const QModelIndex& bottomRight); void jumpAfterModChanged(int state); void queuedScrollTo(int state); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/tablebottombox.cpp000066400000000000000000000171241503074453300245110ustar00rootroot00000000000000#include "tablebottombox.hpp" #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() { // Make sure that the size of the bottom box is determined by the currently visible widget for (int i = 0; i < mLayout->count(); ++i) { QSizePolicy::Policy verPolicy = QSizePolicy::Ignored; if (mLayout->widget(i) == mLayout->currentWidget()) { verPolicy = QSizePolicy::Expanding; } mLayout->widget(i)->setSizePolicy(QSizePolicy::Expanding, verPolicy); } } void CSVWorld::TableBottomBox::updateStatus() { if (mShowStatusBar) { if (!mStatusMessage.isEmpty()) { mStatus->setText(mStatusMessage); return; } 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) { if (mStatusCount[i] > 0) { if (first) first = false; else stream << ", "; stream << mStatusCount[i] << ' ' << (mStatusCount[i] == 1 ? sLabels[i] : sLabelsPlural[i]); } } if (mHasPosition) { if (!first) stream << " -- "; stream << "(" << mRow << ", " << mColumn << ")"; } mStatus->setText(QString::fromUtf8(stream.str().c_str())); } } void CSVWorld::TableBottomBox::extendedConfigRequest( CSVWorld::ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds) { mExtendedConfigurator->configure(mode, selectedIds); mLayout->setCurrentWidget(mExtendedConfigurator); mEditMode = EditMode_ExtendedConfig; 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) { for (int i = 0; i < 4; ++i) mStatusCount[i] = 0; setVisible(false); mLayout = new QStackedLayout; mLayout->setContentsMargins(0, 0, 0, 0); connect(mLayout, &QStackedLayout::currentChanged, this, &TableBottomBox::currentWidgetChanged); mStatus = new QLabel; mStatusBar = new QStatusBar(this); mStatusBar->addWidget(mStatus); mStatusBar->setStyleSheet(statusBarStyle); mLayout->addWidget(mStatusBar); setLayout(mLayout); mCreator = creatorFactory.makeCreator(document, id); if (mCreator) { mCreator->installEventFilter(this); mLayout->addWidget(mCreator); connect(mCreator, &Creator::done, this, &TableBottomBox::requestDone); connect(mCreator, &Creator::requestFocus, this, &TableBottomBox::requestFocus); } mExtendedConfigurator = new ExtendedCommandConfigurator(document, id, this); mExtendedConfigurator->installEventFilter(this); mLayout->addWidget(mExtendedConfigurator); connect(mExtendedConfigurator, &ExtendedCommandConfigurator::done, this, &TableBottomBox::requestDone); updateSize(); } 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); } CSVWorld::TableBottomBox::~TableBottomBox() { delete mCreator; } bool CSVWorld::TableBottomBox::eventFilter(QObject* object, QEvent* event) { if (event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { requestDone(); return true; } } return QWidget::eventFilter(object, event); } void CSVWorld::TableBottomBox::setStatusBar(bool show) { if (show != mShowStatusBar) { setVisible(show || (mEditMode != EditMode_None)); mShowStatusBar = show; if (show) updateStatus(); } } bool CSVWorld::TableBottomBox::canCreateAndDelete() const { return mCreator; } void CSVWorld::TableBottomBox::requestDone() { if (!mShowStatusBar) setVisible(false); else updateStatus(); mLayout->setCurrentWidget(mStatusBar); mEditMode = EditMode_None; } void CSVWorld::TableBottomBox::currentWidgetChanged(int /*index*/) { updateSize(); } void CSVWorld::TableBottomBox::setStatusMessage(const QString& message) { mStatusMessage = message; updateStatus(); } void CSVWorld::TableBottomBox::selectionSizeChanged(int size) { if (mStatusCount[3] != size) { mStatusMessage = ""; mStatusCount[3] = size; updateStatus(); } } void CSVWorld::TableBottomBox::tableSizeChanged(int size, int deleted, int modified) { bool changed = false; if (mStatusCount[0] != size) { mStatusCount[0] = size; changed = true; } if (mStatusCount[1] != deleted) { mStatusCount[1] = deleted; changed = true; } if (mStatusCount[2] != modified) { mStatusCount[2] = modified; changed = true; } if (changed) { mStatusMessage = ""; updateStatus(); } } void CSVWorld::TableBottomBox::positionChanged(int row, int column) { mRow = row; mColumn = column; mHasPosition = true; updateStatus(); } void CSVWorld::TableBottomBox::noMorePosition() { mHasPosition = false; updateStatus(); } void CSVWorld::TableBottomBox::createRequest() { mCreator->reset(); mCreator->toggleWidgets(true); mLayout->setCurrentWidget(mCreator); setVisible(true); mEditMode = EditMode_Creation; mCreator->focus(); } 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); mEditMode = EditMode_Creation; mCreator->focus(); } void CSVWorld::TableBottomBox::touchRequest(const std::vector& ids) { mCreator->touch(ids); } void CSVWorld::TableBottomBox::extendedDeleteConfigRequest(const std::vector& selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Delete, selectedIds); } void CSVWorld::TableBottomBox::extendedRevertConfigRequest(const std::vector& selectedIds) { extendedConfigRequest(ExtendedCommandConfigurator::Mode_Revert, selectedIds); } openmw-openmw-0.49.0/apps/opencs/view/world/tablebottombox.hpp000066400000000000000000000060661503074453300245210ustar00rootroot00000000000000#ifndef CSV_WORLD_BOTTOMBOX_H #define CSV_WORLD_BOTTOMBOX_H #include #include #include #include #include "extendedcommandconfigurator.hpp" class QLabel; class QStackedLayout; class QStatusBar; namespace CSMDoc { class Document; } namespace CSVWorld { class CreatorFactoryBase; class Creator; class TableBottomBox : public QWidget { Q_OBJECT enum EditMode { EditMode_None, EditMode_Creation, EditMode_ExtendedConfig }; bool mShowStatusBar; QLabel* mStatus; QStatusBar* mStatusBar; int mStatusCount[4]; EditMode mEditMode; Creator* mCreator; ExtendedCommandConfigurator* mExtendedConfigurator; QStackedLayout* mLayout; bool mHasPosition; int mRow; int mColumn; QString mStatusMessage; private: // not implemented TableBottomBox(const TableBottomBox&); TableBottomBox& operator=(const TableBottomBox&); void updateSize(); void updateStatus(); void extendedConfigRequest(ExtendedCommandConfigurator::Mode mode, const std::vector& selectedIds); protected: bool event(QEvent* event) override; public: TableBottomBox(const CreatorFactoryBase& creatorFactory, CSMDoc::Document& document, const CSMWorld::UniversalId& id, QWidget* parent = nullptr); ~TableBottomBox() override; bool eventFilter(QObject* object, QEvent* event) override; void setEditLock(bool locked); void setStatusBar(bool show); 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); 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. private slots: void requestDone(); ///< \note This slot being called does not imply success. void currentWidgetChanged(int index); public slots: 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 positionChanged(int row, int column); void noMorePosition(); 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/tableeditidaction.cpp000066400000000000000000000034321503074453300251310ustar00rootroot00000000000000#include "tableeditidaction.hpp" #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); if (index.isValid()) { QVariant display = mTable.model()->data(index, CSMWorld::ColumnBase::Role_Display); QString value = mTable.model()->data(index).toString(); return std::make_pair(static_cast(display.toInt()), value); } 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) { } void CSVWorld::TableEditIdAction::setCell(int row, int column) { CellData data = getCellData(row, column); CSMWorld::UniversalId::Type idType = CSMWorld::TableMimeData::convertEnums(data.first); if (idType != CSMWorld::UniversalId::Type_None) { mCurrentId = CSMWorld::UniversalId(idType, data.second.toUtf8().constData()); setText("Edit '" + data.second + "'"); } } CSMWorld::UniversalId CSVWorld::TableEditIdAction::getCurrentId() const { return mCurrentId; } 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(); } openmw-openmw-0.49.0/apps/opencs/view/world/tableeditidaction.hpp000066400000000000000000000014261503074453300251370ustar00rootroot00000000000000#ifndef CSVWORLD_TABLEEDITIDACTION_HPP #define CSVWORLD_TABLEEDITIDACTION_HPP #include #include #include #include "../../model/world/columnbase.hpp" #include "../../model/world/universalid.hpp" class QTableView; namespace CSVWorld { class TableEditIdAction : public QAction { const QTableView& mTable; CSMWorld::UniversalId mCurrentId; typedef std::pair CellData; CellData getCellData(int row, int column) const; public: TableEditIdAction(const QTableView& table, QWidget* parent = nullptr); void setCell(int row, int column); CSMWorld::UniversalId getCurrentId() const; bool isValidIdCell(int row, int column) const; }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/tableheadermouseeventhandler.cpp000066400000000000000000000043001503074453300273650ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/view/world/tableheadermouseeventhandler.hpp000066400000000000000000000011131503074453300273710ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/opencs/view/world/tablesubview.cpp000066400000000000000000000210321503074453300241510ustar00rootroot00000000000000#include "tablesubview.hpp" #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 "../doc/view.hpp" #include "../filter/filterbox.hpp" #include "../filter/filterdata.hpp" #include "table.hpp" #include "tablebottombox.hpp" 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; 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); QVBoxLayout* buttonLayout = new QVBoxLayout; // work around margin issues buttonLayout->setContentsMargins(QMargins(0 /*left*/, 3 /*top*/, 3 /*right*/, 0 /*bottom*/)); buttonLayout->insertWidget(0, opt, 0, Qt::AlignVCenter | Qt::AlignRight); hLayout->insertWidget(1, mOptions); hLayout->insertLayout(2, buttonLayout); layout->insertLayout(0, hLayout); CSVDoc::SizeHintWidget* widget = new CSVDoc::SizeHintWidget; widget->setLayout(layout); setWidget(widget); QScreen* screen = CSVDoc::View::getWidgetScreen(pos()); // prefer height of the screen and full width of the table const QRect rect = screen->geometry(); int frameHeight = 40; // set a reasonable default QWidget* topLevel = QApplication::topLevelAt(pos()); if (topLevel) frameHeight = topLevel->frameGeometry().height() - topLevel->height(); widget->setSizeHint(QSize(mTable->horizontalHeader()->length(), rect.height() - frameHeight)); connect(mTable, &Table::editRequest, this, &TableSubView::editRequest); connect(mTable, &Table::selectionSizeChanged, mBottom, &TableBottomBox::selectionSizeChanged); connect(mTable, &Table::tableSizeChanged, mBottom, &TableBottomBox::tableSizeChanged); mTable->tableSizeUpdate(); mTable->selectionSizeUpdate(); mTable->viewport()->installEventFilter(this); mBottom->installEventFilter(this); mFilterBox->installEventFilter(this); if (mBottom->canCreateAndDelete()) { connect(mTable, &Table::createRequest, mBottom, &TableBottomBox::createRequest); connect( mTable, &Table::cloneRequest, this, qOverload(&TableSubView::cloneRequest)); connect(this, qOverload(&TableSubView::cloneRequest), mBottom, &TableBottomBox::cloneRequest); connect(mTable, &Table::createRecordsDirectlyRequest, mBottom, &TableBottomBox::createRecordsDirectlyRequest); connect(mTable, &Table::touchRequest, mBottom, &TableBottomBox::touchRequest); connect(mTable, &Table::extendedDeleteConfigRequest, mBottom, &TableBottomBox::extendedDeleteConfigRequest); connect(mTable, &Table::extendedRevertConfigRequest, mBottom, &TableBottomBox::extendedRevertConfigRequest); } connect(mBottom, &TableBottomBox::requestFocus, mTable, &Table::requestFocus); connect(mFilterBox, &CSVFilter::FilterBox::recordFilterChanged, mTable, &Table::recordFilterChanged); connect(mFilterBox, &CSVFilter::FilterBox::recordDropped, this, &TableSubView::createFilterRequest); connect(mTable, &Table::closeRequest, this, qOverload<>(&TableSubView::closeRequest)); } void CSVWorld::TableSubView::setEditLock(bool locked) { mTable->setEditLock(locked); mBottom->setEditLock(locked); } void CSVWorld::TableSubView::editRequest(const CSMWorld::UniversalId& id, const std::string& hint) { focusId(id, hint); } void CSVWorld::TableSubView::setStatusBar(bool show) { mBottom->setStatusBar(show); } void CSVWorld::TableSubView::useHint(const std::string& hint) { if (hint.empty()) return; if (hint[0] == 'f' && hint.size() >= 2) mFilterBox->setRecordFilter(hint.substr(2)); } void CSVWorld::TableSubView::cloneRequest(const CSMWorld::UniversalId& toClone) { emit cloneRequest(toClone.getId(), toClone.getType()); } void CSVWorld::TableSubView::createFilterRequest(std::vector& types, const std::pair& columnSearchData, Qt::DropAction action) { 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()) { CSVFilter::FilterData filterData; filterData.searchData = it->getId(); filterData.columns = std::move(col); sourceFilter.emplace_back(filterData); } if (hasRefIdDisplay && CSMWorld::TableMimeData::isReferencable(type)) { CSVFilter::FilterData filterData; filterData.searchData = it->getId(); filterData.columns = refIdColumns; sourceFilter.emplace_back(filterData); } } 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) { if (event->type() == QEvent::Drop) { if (QDropEvent* drop = dynamic_cast(event)) { 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; bool handled = tableMimeData->holdsType(CSMWorld::UniversalId::Type_Filter); if (handled) { mFilterBox->setRecordFilter(tableMimeData->returnMatching(CSMWorld::UniversalId::Type_Filter).getId()); } return handled; } } return false; } void CSVWorld::TableSubView::toggleOptions() { if (mShowOptions) { mShowOptions = false; mOptions->hide(); } else { mShowOptions = true; mOptions->show(); } } void CSVWorld::TableSubView::requestFocus(const std::string& id) { mTable->requestFocus(id); } openmw-openmw-0.49.0/apps/opencs/view/world/tablesubview.hpp000066400000000000000000000031701503074453300241610ustar00rootroot00000000000000#ifndef CSV_WORLD_TABLESUBVIEW_H #define CSV_WORLD_TABLESUBVIEW_H #include "../doc/subview.hpp" #include #include #include #include #include #include class QEvent; class QObject; class QWidget; namespace CSMDoc { class Document; } namespace CSVFilter { class FilterBox; } namespace CSVWorld { class Table; class TableBottomBox; class CreatorFactoryBase; class TableSubView : public CSVDoc::SubView { Q_OBJECT Table* mTable; TableBottomBox* mBottom; CSVFilter::FilterBox* mFilterBox; bool mShowOptions; QWidget* mOptions; public: TableSubView(const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting); void setEditLock(bool locked) override; void setStatusBar(bool show) override; void useHint(const std::string& hint) override; protected: bool eventFilter(QObject* object, QEvent* event) override; signals: void cloneRequest(const std::string&, const CSMWorld::UniversalId::Type); private slots: 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: void requestFocus(const std::string& id); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/util.cpp000066400000000000000000000272751503074453300224510ustar00rootroot00000000000000#include "util.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../../model/world/commanddispatcher.hpp" #include "../widget/coloreditor.hpp" #include "../widget/droplineedit.hpp" #include "dialoguespinbox.hpp" #include "scriptedit.hpp" CSVWorld::NastyTableModelHack::NastyTableModelHack(QAbstractItemModel& model) : mModel(model) { } int CSVWorld::NastyTableModelHack::rowCount(const QModelIndex& parent) const { return mModel.rowCount(parent); } int CSVWorld::NastyTableModelHack::columnCount(const QModelIndex& parent) const { return mModel.columnCount(parent); } QVariant CSVWorld::NastyTableModelHack::data(const QModelIndex& index, int role) const { return mModel.data(index, role); } bool CSVWorld::NastyTableModelHack::setData(const QModelIndex& index, const QVariant& value, int role) { mData = value; return true; } QVariant CSVWorld::NastyTableModelHack::getData() const { return mData; } CSVWorld::CommandDelegateFactoryCollection* CSVWorld::CommandDelegateFactoryCollection::sThis = nullptr; CSVWorld::CommandDelegateFactoryCollection::CommandDelegateFactoryCollection() { if (sThis) throw std::logic_error("multiple instances of CSVWorld::CommandDelegateFactoryCollection"); sThis = this; } CSVWorld::CommandDelegateFactoryCollection::~CommandDelegateFactoryCollection() { sThis = nullptr; for (std::map::iterator iter(mFactories.begin()); iter != mFactories.end(); ++iter) delete iter->second; } void CSVWorld::CommandDelegateFactoryCollection::add( CSMWorld::ColumnBase::Display display, CommandDelegateFactory* 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 { std::map::const_iterator iter = mFactories.find(display); if (iter != mFactories.end()) return iter->second->makeDelegate(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"); return *sThis; } QUndoStack& CSVWorld::CommandDelegate::getUndoStack() const { return mDocument.getUndoStack(); } CSMDoc::Document& CSVWorld::CommandDelegate::getDocument() const { return mDocument; } 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 { if (!mCommandDispatcher) return; QVariant variant; // Color columns use a custom editor, so we need to fetch selected color from it. CSVWidget::ColorEditor* colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { variant = colorEditor->colorInt(); } else { 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); } 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 { if (!mEditLock) { 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 { CSMWorld::ColumnBase::Display display = getDisplayTypeFromIndex(index); // This createEditor() method is called implicitly from tables. // For boolean values in tables use the default editor (combobox). // Checkboxes is looking ugly in the table view. // TODO: Find a better solution? if (display == CSMWorld::ColumnBase::Display_Boolean) { return QItemEditorFactory::defaultFactory()->createEditor(QVariant::Bool, parent); } // For tables the pop-up of the color editor should appear immediately after the editor creation // (the third parameter of ColorEditor's constructor) else if (display == CSMWorld::ColumnBase::Display_Colour) { return new CSVWidget::ColorEditor(index.data().toInt(), parent, true); } return createEditor(parent, option, index, display); } QWidget* CSVWorld::CommandDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index, CSMWorld::ColumnBase::Display display) const { QVariant variant = index.data(); if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return nullptr; } } // NOTE: for each editor type (e.g. QLineEdit) there needs to be a corresponding // entry in CSVWorld::DialogueDelegateDispatcher::makeEditor() switch (display) { case CSMWorld::ColumnBase::Display_Colour: { return new CSVWidget::ColorEditor(variant.toInt(), parent); } case CSMWorld::ColumnBase::Display_Integer: { 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); sb->setRange(std::numeric_limits::min(), std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_SignedInteger16: { 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); sb->setRange(0, std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_UnsignedInteger16: { DialogueSpinBox* sb = new DialogueSpinBox(parent); sb->setRange(0, std::numeric_limits::max()); return sb; } case CSMWorld::ColumnBase::Display_Var: return new QLineEdit(parent); case CSMWorld::ColumnBase::Display_Float: { DialogueDoubleSpinBox* dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(3); return dsb; } case CSMWorld::ColumnBase::Display_Double: { DialogueDoubleSpinBox* dsb = new DialogueDoubleSpinBox(parent); dsb->setRange(-std::numeric_limits::max(), std::numeric_limits::max()); dsb->setSingleStep(0.01f); dsb->setDecimals(6); return dsb; } /// \todo implement size limit. QPlainTextEdit does not support a size limit. case CSMWorld::ColumnBase::Display_LongString: case CSMWorld::ColumnBase::Display_LongString256: { QPlainTextEdit* edit = new QPlainTextEdit(parent); edit->setUndoRedoEnabled(false); return edit; } case CSMWorld::ColumnBase::Display_Boolean: return new QCheckBox(parent); case CSMWorld::ColumnBase::Display_ScriptLines: 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 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); 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); } } void CSVWorld::CommandDelegate::setEditLock(bool locked) { mEditLock = locked; } bool CSVWorld::CommandDelegate::isEditLocked() const { return mEditLock; } void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { setEditorData(editor, index, false); } void CSVWorld::CommandDelegate::setEditorData(QWidget* editor, const QModelIndex& index, bool tryDisplay) const { QVariant variant = index.data(Qt::EditRole); if (tryDisplay) { if (!variant.isValid()) { variant = index.data(Qt::DisplayRole); if (!variant.isValid()) { return; } } QPlainTextEdit* plainTextEdit = qobject_cast(editor); if (plainTextEdit) // for some reason it is easier to brake the loop here { if (plainTextEdit->toPlainText() == variant.toString()) { return; } } } // Color columns use a custom editor, so we need explicitly set a data for it CSVWidget::ColorEditor* colorEditor = qobject_cast(editor); if (colorEditor != nullptr) { colorEditor->setColor(variant.toInt()); return; } QByteArray n = editor->metaObject()->userProperty().name(); if (n == "dateTime") { if (editor->inherits("QTimeEdit")) n = "time"; else if (editor->inherits("QDateEdit")) n = "date"; } if (!n.isEmpty()) { if (!variant.isValid()) #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) variant = QVariant(editor->property(n).metaType(), (const void*)nullptr); #else variant = QVariant(editor->property(n).userType(), (const void*)nullptr); #endif editor->setProperty(n, variant); } } void CSVWorld::CommandDelegate::settingChanged(const CSMPrefs::Setting* setting) {} openmw-openmw-0.49.0/apps/opencs/view/world/util.hpp000066400000000000000000000107001503074453300224370ustar00rootroot00000000000000#ifndef CSV_WORLD_UTIL_H #define CSV_WORLD_UTIL_H #include #include #include #ifndef Q_MOC_RUN #include "../../model/world/columnbase.hpp" #endif class QUndoStack; class QWidget; namespace CSMDoc { class Document; } namespace CSMWorld { class CommandDispatcher; } namespace CSMPrefs { class Setting; } namespace CSVWorld { ///< \brief Getting the data out of an editor widget /// /// Really, Qt? Really? class NastyTableModelHack : public QAbstractTableModel { QAbstractItemModel& mModel; QVariant mData; public: NastyTableModelHack(QAbstractItemModel& model); int rowCount(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; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; QVariant getData() const; }; class CommandDelegate; class 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. }; class CommandDelegateFactoryCollection { static CommandDelegateFactoryCollection* sThis; std::map mFactories; private: // not implemented CommandDelegateFactoryCollection(const CommandDelegateFactoryCollection&); CommandDelegateFactoryCollection& operator=(const 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. 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: QUndoStack& getUndoStack() const; CSMDoc::Document& getDocument() const; CSMWorld::ColumnBase::Display getDisplayTypeFromIndex(const QModelIndex& index) const; virtual void setModelDataImp(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const; 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; 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; void setEditLock(bool locked); bool isEditLocked() const; ///< \return Does column require update? void setEditorData(QWidget* editor, const QModelIndex& index) const override; 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); }; } #endif openmw-openmw-0.49.0/apps/opencs/view/world/vartypedelegate.cpp000066400000000000000000000047671503074453300246620ustar00rootroot00000000000000#include "vartypedelegate.hpp" #include #include #include #include #include #include "../../model/world/columns.hpp" #include "../../model/world/commandmacro.hpp" #include "../../model/world/commands.hpp" namespace CSMDoc { class Document; } namespace CSMWorld { 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 value; switch (type) { case ESM::VT_Short: case ESM::VT_Int: case ESM::VT_Long: value = old.toInt(); break; case ESM::VT_Float: value = old.toFloat(); break; case ESM::VT_String: value = old.toString(); break; default: break; // ignore the rest } 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)); } 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) { if (type0 != ESM::VT_Unknown) add(type0); if (type1 != ESM::VT_Unknown) add(type1); if (type2 != ESM::VT_Unknown) add(type2); if (type3 != ESM::VT_Unknown) add(type3); } CSVWorld::CommandDelegate* CSVWorld::VarTypeDelegateFactory::makeDelegate( CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent) const { return new VarTypeDelegate(mValues, dispatcher, document, parent); } void CSVWorld::VarTypeDelegateFactory::add(ESM::VarType type) { std::vector> enums = CSMWorld::Columns::getEnums(CSMWorld::Columns::ColumnId_ValueType); if (static_cast(type) >= enums.size()) throw std::logic_error("Unsupported variable type"); mValues.emplace_back(type, QString::fromUtf8(enums[type].second.c_str())); } openmw-openmw-0.49.0/apps/opencs/view/world/vartypedelegate.hpp000066400000000000000000000024731503074453300246570ustar00rootroot00000000000000#ifndef CSV_WORLD_VARTYPEDELEGATE_H #define CSV_WORLD_VARTYPEDELEGATE_H #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; public: VarTypeDelegate(const std::vector>& values, CSMWorld::CommandDispatcher* dispatcher, CSMDoc::Document& document, QObject* parent); }; class VarTypeDelegateFactory : public CommandDelegateFactory { std::vector> mValues; 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. void add(ESM::VarType type); }; } #endif openmw-openmw-0.49.0/apps/opencs_tests/000077500000000000000000000000001503074453300200745ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs_tests/CMakeLists.txt000066400000000000000000000016361503074453300226420ustar00rootroot00000000000000file(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() openmw-openmw-0.49.0/apps/opencs_tests/main.cpp000066400000000000000000000006001503074453300215200ustar00rootroot00000000000000#include #include #include int main(int argc, char* argv[]) { Log::sMinDebugLevel = Debug::getDebugLevel(); testing::InitGoogleTest(&argc, argv); const int result = RUN_ALL_TESTS(); if (result == 0) std::filesystem::remove_all(TestingOpenMW::outputDir()); return result; } openmw-openmw-0.49.0/apps/opencs_tests/model/000077500000000000000000000000001503074453300211745ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs_tests/model/world/000077500000000000000000000000001503074453300223235ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/opencs_tests/model/world/testinfocollection.cpp000066400000000000000000000627151503074453300267510ustar00rootroot00000000000000#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"))))); } } } openmw-openmw-0.49.0/apps/opencs_tests/model/world/testuniversalid.cpp000066400000000000000000000201561503074453300262600ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/openmw/000077500000000000000000000000001503074453300166705ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/CMakeLists.txt000066400000000000000000000237571503074453300214460ustar00rootroot00000000000000set(OPENMW_SOURCES engine.cpp options.cpp ) set(OPENMW_RESOURCES ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest ) set(OPENMW_HEADERS doc.hpp engine.hpp options.hpp profile.hpp ) 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 skyutil npcanimation esm4npcanimation vismask creatureanimation effectmanager util renderinginterface pathgrid rendermode weaponanimation screenshotmanager 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 sensormanager gyromanager ) add_openmw_dir (mwgui layout textinput widgets race class birth review windowmanagerimp console dialogue windowbase statswindow messagebox journalwindow charactercreation mapwindow windowpinnablebase tooltips scrollwindow bookwindow resourceskin formatting inventorywindow container hud countdialog tradewindow settingswindow confirmationdialog alchemywindow referenceinterface spellwindow mainmenu quickkeysmenu itemselection spellbuyingwindow loadingscreen levelupdialog waitdialog spellcreationdialog enchantingdialog trainingwindow travelwindow exposedwindow cursor spellicons merchantrepair repair soulgemdialog companionwindow bookpage journalviewmodel journalbooks itemmodel containeritemmodel inventoryitemmodel sortfilteritemmodel itemview 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 dialoguemanagerimp journalimp journalentry quest topic filter selectwrapper hypertextparser keywordsearch scripttest ) add_openmw_dir (mwscript locals scriptmanagerimp compilercontext interpretercontext cellextensions miscextensions guiextensions soundextensions skyextensions statsextensions containerextensions aiextensions controlextensions extensions globalscripts ref dialogueextensions 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 ) add_openmw_dir (mwworld refdata worldimp scene globals class action nullaction actionteleport containerstore actiontalk actiontake manualref player cellvisitors failedaction worldmodel localscripts customdata inventorystore ptr actionopen actionread actionharvest actionequip timestamp actionalchemy cellstore actionapply actioneat store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry positioncellgrid ) add_openmw_dir (mwphysics physicssystem trace collisiontype actor convert object heightfield closestnotmerayresultcallback 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 summoning character actors objects aistate weaponpriority spellpriority weapontype spellutil spelleffects ) add_openmw_dir (mwstate statemanagerimp charactermanager character quicksavemanager ) add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager inputmanager windowmanager statemanager luamanager ) # Main executable add_library(openmw-lib STATIC ${OPENMW_FILES} ${OPENMW_SOURCES} ) if(BUILD_OPENMW) if (ANDROID) add_library(openmw SHARED main.cpp android_main.cpp ) else() openmw_add_executable(openmw ${APPLE_BUNDLE_RESOURCES} ${OPENMW_RESOURCES} main.cpp ) endif() target_link_libraries(openmw openmw-lib) # Workaround necessary to ensure osgAnimation::MatrixLinearSampler dynamic casts work under Clang # NOTE: it's unclear whether the broken behavior is spec-compliant if (CMAKE_CXX_COMPILER_ID STREQUAL Clang) set_target_properties(openmw PROPERTIES ENABLE_EXPORTS ON) endif() endif() # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING # when we change the backend. include_directories( ${FFmpeg_INCLUDE_DIRS} ) 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`. # https://gitlab.kitware.com/cmake/cmake/-/issues/21701 ${OSGPARTICLE_LIBRARIES} ${OSGVIEWER_LIBRARIES} ${OSGGA_LIBRARIES} ${OSGSHADOW_LIBRARIES} ${OSGDB_LIBRARIES} ${OSGUTIL_LIBRARIES} ${OSG_LIBRARIES} Boost::program_options ${OPENAL_LIBRARY} ${FFmpeg_LIBRARIES} ${MyGUI_LIBRARIES} SDL2::SDL2 ${RecastNavigation_LIBRARIES} "osg-ffmpeg-videoplayer" "oics" components ) 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-lib EGL android log z) endif (ANDROID) if (USE_SYSTEM_TINYXML) target_link_libraries(openmw-lib ${TinyXML_LIBRARIES}) endif() if (NOT UNIX) 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-lib ${CMAKE_THREAD_LIBS_INIT}) endif() if(APPLE AND BUILD_OPENMW) set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources") set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR}) add_subdirectory(../../files/ ${CMAKE_CURRENT_BINARY_DIR}/files) configure_file("${OpenMW_BINARY_DIR}/defaults.bin" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/openmw.cfg" ${BUNDLE_RESOURCES_DIR} COPYONLY) configure_file("${OpenMW_BINARY_DIR}/gamecontrollerdb.txt" ${BUNDLE_RESOURCES_DIR} COPYONLY) add_custom_command(TARGET openmw POST_BUILD COMMAND cp "${OpenMW_BINARY_DIR}/resources/version" "${BUNDLE_RESOURCES_DIR}/resources") find_library(COCOA_FRAMEWORK Cocoa) find_library(IOKIT_FRAMEWORK IOKit) target_link_libraries(openmw ${COCOA_FRAMEWORK} ${IOKIT_FRAMEWORK}) if (FFmpeg_FOUND) 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() if (BUILD_WITH_CODE_COVERAGE) 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 (WIN32 AND BUILD_OPENMW) INSTALL(TARGETS openmw RUNTIME DESTINATION ".") endif() openmw-openmw-0.49.0/apps/openmw/android_main.cpp000066400000000000000000000041771503074453300220310ustar00rootroot00000000000000#ifndef stderr int stderr = 0; // Hack: fix linker error #endif #include "SDL_main.h" #include #include #include /******************************************************************************* Functions called by JNI *******************************************************************************/ #include /* Called before to initialize JNI bindings */ extern void SDL_Android_Init(JNIEnv* env, jclass cls); extern int argcData; extern const char** argvData; void releaseArgv(); 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) { int ret = 0; SDL_GetMouseState(nullptr, &ret); return ret; } 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) { 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) { SDL_SendMouseButton(Android_Window, 0, state, button); } 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"); return 0; } openmw-openmw-0.49.0/apps/openmw/doc.hpp000066400000000000000000000016441503074453300201530ustar00rootroot00000000000000// Note: This is not a regular source file. /// \ingroup apps /// \defgroup openmw OpenMW /// \namespace OMW /// \ingroup openmw /// \brief Integration of OpenMW-subsystems /// \namespace MWDialogue /// \ingroup openmw /// \brief NPC dialogues /// \namespace MWMechanics /// \ingroup openmw /// \brief Game mechanics and NPC-AI /// \namespace MWSound /// \ingroup openmw /// \brief Sound & music /// \namespace MWGUI /// \ingroup openmw /// \brief HUD and windows /// \namespace MWRender /// \ingroup openmw /// \brief Rendering /// \namespace MWWorld /// \ingroup openmw /// \brief World data /// \namespace MWClass /// \ingroup openmw /// \brief Workaround for non-OOP design of the record system /// \namespace MWInput /// \ingroup openmw /// \brief User input and character controls /// \namespace MWScript /// \ingroup openmw /// \brief MW-specific script extensions and integration of the script system into OpenMW openmw-openmw-0.49.0/apps/openmw/engine.cpp000066400000000000000000001204561503074453300206510ustar00rootroot00000000000000#include "engine.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 "mwinput/inputmanagerimp.hpp" #include "mwgui/windowmanagerimp.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/datetimemanager.hpp" #include "mwworld/worldimp.hpp" #include "mwrender/vismask.hpp" #include "mwclass/classes.hpp" #include "mwdialogue/dialoguemanagerimp.hpp" #include "mwdialogue/journalimp.hpp" #include "mwdialogue/scripttest.hpp" #include "mwmechanics/mechanicsmanagerimp.hpp" #include "mwstate/statemanagerimp.hpp" #include "profile.hpp" namespace { void checkSDLError(int ret) { if (ret != 0) Log(Debug::Error) << "SDL error: " << SDL_GetError(); } void initStatsHandler(Resource::Profiler& profiler) { 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; 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"); } struct ScreenCaptureMessageBox { void operator()(std::string filePath) const { if (filePath.empty()) { MWBase::Environment::get().getWindowManager()->scheduleMessageBox( "#{OMWEngine:ScreenshotFailed}", MWGui::ShowInDialogueMode_Never); return; } std::string messageFormat = MWBase::Environment::get().getL10nManager()->getMessage("OMWEngine", "ScreenshotMade"); std::string message = Misc::StringUtils::format(messageFormat, filePath); MWBase::Environment::get().getWindowManager()->scheduleMessageBox( std::move(message), MWGui::ShowInDialogueMode_Never); } }; struct IgnoreString { void operator()(std::string) const {} }; class IdentifyOpenGLOperation : public osg::GraphicsOperation { public: IdentifyOpenGLOperation() : GraphicsOperation("IdentifyOpenGLOperation", false) { } 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); } int getMaxTextureImageUnits() const { if (mMaxTextureImageUnits == 0) throw std::logic_error("mMaxTextureImageUnits is not initialized"); return mMaxTextureImageUnits; } private: int mMaxTextureImageUnits = 0; }; void reportStats(unsigned frameNumber, osgViewer::Viewer& viewer, std::ostream& stream) { 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 = mWorld->getLocalScripts(); localScripts.startIteration(); std::pair script; while (localScripts.getNext(script)) { MWScript::InterpreterContext interpreterContext(&script.second.getRefData().getLocals(), script.second); mScriptManager->run(script.first, interpreterContext); } } bool OMW::Engine::frame(unsigned frameNumber, float frametime) { const osg::Timer_t frameStart = mViewer->getStartTick(); const osg::Timer* const timer = osg::Timer::instance(); osg::Stats* const stats = mViewer->getViewerStats(); mEnvironment.setFrameDuration(frametime); try { // update input { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); 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) { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (!mWindowManager->isWindowVisible()) { mSoundManager->pausePlayback(); return false; } else mSoundManager->resumePlayback(); // sound if (mUseSound) mSoundManager->update(frametime); } { 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); mStateManager->update(frametime); } bool paused = mWorld->getTimeManager()->isPaused(); { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { if (!mWindowManager->containsMode(MWGui::GM_MainMenu) || !paused) { if (mWorld->getScriptsEnabled()) { // local scripts executeLocalScripts(); // global scripts mScriptManager->getGlobalScripts().run(); } mWorld->getWorldScene().markCellAsUnchanged(); } if (!paused) { double hours = (frametime * mWorld->getTimeManager()->getGameTimeScale()) / 3600.0; mWorld->advanceTime(hours, true); mWorld->rechargeItems(frametime, true); } } } // update mechanics { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { mMechanicsManager->update(frametime, paused); } if (mStateManager->getState() == MWBase::StateManager::State_Running) { MWWorld::Ptr player = mWorld->getPlayerPtr(); if (!paused && player.getClass().getCreatureStats(player).isDead()) mStateManager->endGame(); } } // update physics { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { mWorld->updatePhysics(frametime, paused, frameStart, frameNumber, *stats); } } // update world { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); if (mStateManager->getState() != MWBase::StateManager::State_NoGame) { mWorld->update(frametime, paused); } } // update GUI { ScopedProfile profile(frameStart, frameNumber, *timer, *stats); mWindowManager->update(frametime); } } catch (const std::exception& e) { Log(Debug::Error) << "Error in frame: " << e.what(); } const bool reportResource = stats->collectStats("resource"); if (reportResource) stats->setAttribute(frameNumber, "UnrefQueue", mUnrefQueue->getSize()); mUnrefQueue->flush(*mWorkQueue); 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); } mStereoManager->updateSettings(Settings::camera().mNearClip, Settings::camera().mViewingDistance); mViewer->eventTraversal(); mViewer->updateTraversal(); // update GUI by world data { 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) , 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) , mExportFonts(false) , mRandomSeed(0) , mNewGame(false) , mCfgMgr(configurationManager) , mGlMaxTextureImageUnits(0) { #if SDL_VERSION_ATLEAST(2, 24, 0) SDL_SetHint(SDL_HINT_MAC_OPENGL_ASYNC_DISPATCH, "1"); #endif 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) { SDL_SetMainReady(); if (SDL_Init(flags) != 0) { throw std::runtime_error("Could not initialize SDL! " + std::string(SDL_GetError())); } } } OMW::Engine::~Engine() { if (mScreenCaptureOperation != nullptr) mScreenCaptureOperation->stop(); mMechanicsManager = nullptr; mDialogueManager = nullptr; mJournal = nullptr; mWindowManager = nullptr; mScriptManager = nullptr; mWorld = nullptr; mStereoManager = nullptr; mSoundManager = nullptr; mInputManager = nullptr; mStateManager = nullptr; mLuaWorker = nullptr; mLuaManager = nullptr; mL10nManager = nullptr; mScriptContext = nullptr; mUnrefQueue = nullptr; mWorkQueue = nullptr; mViewer = nullptr; mResourceSystem.reset(); mEncoder = nullptr; if (mWindow) { SDL_DestroyWindow(mWindow); mWindow = nullptr; } SDL_Quit(); } // Set data dir void OMW::Engine::setDataDirs(const Files::PathContainer& dataDirs) { mDataDirs = dataDirs; mDataDirs.insert(mDataDirs.begin(), mResDir / "vfs"); mFileCollections = Files::Collections(mDataDirs); } // Add BSA archive void OMW::Engine::addArchive(const std::string& archive) { mArchives.push_back(archive); } // Set resource dir 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) { mCellName = cellName; } void OMW::Engine::addContentFile(const std::string& file) { mContentFiles.push_back(file); } void OMW::Engine::addGroundcoverFile(const std::string& file) { mGroundcoverFiles.emplace_back(file); } void OMW::Engine::setSkipMenu(bool skipMenu, bool newGame) { mSkipMenu = skipMenu; mNewGame = newGame; } void OMW::Engine::createWindow() { 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); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); 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 | 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::video().mMinimizeOnFocusLoss ? "1" : "0"); checkSDLError(SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24)); if (Debug::shouldDebugOpenGL()) checkSDLError(SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG)); if (antialiasing > 0) { checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); } osg::ref_ptr graphicsWindow; while (!graphicsWindow || !graphicsWindow->valid()) { while (!mWindow) { mWindow = SDL_CreateWindow("OpenMW", pos_x, pos_y, width, height, flags); if (!mWindow) { // Try with a lower AA if (antialiasing > 0) { Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing / 2; antialiasing /= 2; Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } else { std::stringstream error; error << "Failed to create SDL window: " << SDL_GetError(); throw std::runtime_error(error.str()); } } } // 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_GL_GetDrawableSize(mWindow, &traits->width, &traits->height); traits->windowName = SDL_GetWindowTitle(mWindow); traits->windowDecoration = !(SDL_GetWindowFlags(mWindow) & SDL_WINDOW_BORDERLESS); traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); traits->vsync = 0; traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); 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."; graphicsWindow->closeImplementation(); SDL_DestroyWindow(mWindow); mWindow = nullptr; antialiasing /= 2; Settings::video().mAntialiasing.set(antialiasing); checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); continue; } if (traits->red < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->red << " bit red channel."; if (traits->green < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->green << " bit green channel."; if (traits->blue < 8) Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->blue << " bit blue channel."; if (traits->depth < 24) Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->depth << " bits of depth precision."; traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel } osg::ref_ptr camera = mViewer->getCamera(); 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()) 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); } void OMW::Engine::setWindowIcon() { 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; osgDB::ReaderWriter* reader = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!reader) { Log(Debug::Error) << "Error: Failed to read window icon, no png readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = reader->readImage(windowIconStream); if (!result.success()) Log(Debug::Error) << "Error: Failed to read " << windowIcon << ": " << result.message() << " code " << result.status(); else { osg::ref_ptr image = result.getImage(); auto surface = SDLUtil::imageToSurface(image, true); SDL_SetWindowIcon(mWindow, surface.get()); } } void OMW::Engine::prepareEngine() { mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); mEnvironment.setStateManager(*mStateManager); 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); mViewer->setSceneData(rootNode); createWindow(); mVFS = std::make_unique(); VFS::registerArchives(mVFS.get(), mFileCollections, mArchives, true); 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); 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 const auto keybinderUser = mCfgMgr.getUserConfigPath() / "input_v3.xml"; bool keybinderUserExists = std::filesystem::exists(keybinderUser); if (!keybinderUserExists) { 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 auto userdefault = mCfgMgr.getUserConfigPath() / "gamecontrollerdb.txt"; const auto localdefault = mCfgMgr.getLocalPath() / "gamecontrollerdb.txt"; const auto globaldefault = mCfgMgr.getGlobalPath() / "gamecontrollerdb.txt"; std::filesystem::path userGameControllerdb; if (std::filesystem::exists(userdefault)) userGameControllerdb = userdefault; std::filesystem::path gameControllerdb; if (std::filesystem::exists(localdefault)) gameControllerdb = localdefault; else if (std::filesystem::exists(globaldefault)) gameControllerdb = globaldefault; // else if it doesn't exist, pass in an empty string // 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 osg::ref_ptr guiRoot = new osg::Group; guiRoot->setName("GUI Root"); guiRoot->setNodeMask(MWRender::Mask_GUI); mStereoManager->disableStereoForNode(guiRoot); rootNode->addChild(guiRoot); mWindowManager = std::make_unique(mWindow, mViewer, guiRoot, mResourceSystem.get(), mWorkQueue.get(), mCfgMgr.getLogPath(), mScriptConsoleMode, mTranslationDataStorage, mEncoding, mExportFonts, 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 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) { std::string_view logo = Fallback::Map::getString("Movies_Company_Logo"); if (!logo.empty()) mWindowManager->playVideo(logo, true); } listener->loadingOn(); { using namespace std::chrono_literals; while (dataLoading.wait_for(50ms) != std::future_status::ready) asyncListener.update(); dataLoading.get(); } listener->loadingOff(); mWorld->init(mMaxRecastLogLevel, 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); } }); mWindowManager->setStore(mWorld->getStore()); mWindowManager->initUI(); // Load translation data mTranslationDataStorage.setEncoder(mEncoder.get()); for (auto& mContentFile : mContentFiles) mTranslationDataStorage.loadTranslationData(mFileCollections, mContentFile); Compiler::registerExtensions(mExtensions); // Create script system mScriptContext = std::make_unique(MWScript::CompilerContext::Type_Full); mScriptContext->setExtensions(&mExtensions); mScriptManager = std::make_unique(mWorld->getStore(), *mScriptContext, mWarningsMode); mEnvironment.setScriptManager(*mScriptManager); // Create game mechanics system mMechanicsManager = std::make_unique(); mEnvironment.setMechanicsManager(*mMechanicsManager); // Create dialog system mJournal = std::make_unique(); mEnvironment.setJournal(*mJournal); mDialogueManager = std::make_unique(mExtensions, mTranslationDataStorage); mEnvironment.setDialogueManager(*mDialogueManager); // scripts if (mCompileAll) { 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 << "%)"; } if (mCompileAllDialogue) { std::pair result = MWDialogue::ScriptTest::compileAll(&mExtensions, mWarningsMode); if (result.first) Log(Debug::Info) << "compiled " << result.second << " of " << result.first << " dialogue scripts (" << 100 * static_cast(result.second) / result.first << "%)"; } mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath()); mLuaManager->init(); // 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()); 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; Misc::Rng::init(mRandomSeed); Settings::ShaderManager::get().load(mCfgMgr.getUserConfigPath() / "shaders.yaml"); MWClass::registerClasses(); // Create encoder mEncoder = std::make_unique(mEncoding); // Setup viewer mViewer = new osgViewer::Viewer; mViewer->setReleaseContextAtEndOfFrameHint(false); // Do not try to outsmart the OS thread scheduler (see bug #4785). mViewer->setUseConfigureAffinity(false); mEnvironment.setFrameRateLimit(Settings::video().mFramerateLimit); prepareEngine(); #ifdef _WIN32 const auto* stats_file = _wgetenv(L"OPENMW_OSG_STATS_FILE"); #else const auto* stats_file = std::getenv("OPENMW_OSG_STATS_FILE"); #endif std::filesystem::path path; if (stats_file != nullptr) path = stats_file; std::ofstream stats; if (!path.empty()) { stats.open(path, std::ios_base::out); if (stats.is_open()) Log(Debug::Info) << "OSG stats will be written to: " << path; else 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(), *mVFS); initStatsHandler(*statsHandler); mViewer->addEventHandler(statsHandler); osg::ref_ptr resourcesHandler = new Resource::StatsHandler(stats.is_open(), *mVFS); mViewer->addEventHandler(resourcesHandler); if (stats.is_open()) Resource::collectStatistics(*mViewer); // Start the game if (!mSaveGameFile.empty()) { mStateManager->loadGame(mSaveGameFile); } else if (!mSkipMenu) { // start in main menu 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()) mWindowManager->playVideo(logo, /*allowSkipping*/ true, /*overrideSounds*/ false); } else { mStateManager->newGame(!mNewGame); } if (!mStartupScript.empty() && mStateManager->getState() == MWState::StateManager::State_Running) { mWindowManager->executeInConsole(mStartupScript); } // Start the main rendering loop 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() && !mStateManager->hasQuitRequest()) { const double dt = std::chrono::duration_cast>( std::min(frameRateLimiter.getLastFrameDuration(), maxSimulationInterval)) .count() * timeManager.getSimulationTimeScale(); mViewer->advance(timeManager.getRenderingSimulationTime()); const unsigned frameNumber = mViewer->getFrameStamp()->getFrameNumber(); if (!frame(frameNumber, dt)) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); continue; } timeManager.updateIsPaused(); if (!timeManager.isPaused()) { timeManager.setSimulationTime(timeManager.getSimulationTime() + dt); timeManager.setRenderingSimulationTime(timeManager.getRenderingSimulationTime() + dt); } if (stats) { // 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) { // 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(); } mLuaWorker->join(); // 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) { mCompileAll = all; } void OMW::Engine::setCompileAllDialogue(bool all) { mCompileAllDialogue = all; } void OMW::Engine::setSoundUsage(bool soundUsage) { mUseSound = soundUsage; } void OMW::Engine::setEncoding(const ToUTF8::FromType& encoding) { mEncoding = encoding; } void OMW::Engine::setScriptConsoleMode(bool enabled) { mScriptConsoleMode = enabled; } void OMW::Engine::setStartupScript(const std::filesystem::path& path) { mStartupScript = path; } void OMW::Engine::setActivationDistanceOverride(int distance) { mActivationDistanceOverride = distance; } void OMW::Engine::setWarningsMode(int mode) { mWarningsMode = mode; } void OMW::Engine::enableFontExport(bool exportFonts) { mExportFonts = exportFonts; } void OMW::Engine::setSaveGameFile(const std::filesystem::path& savegame) { mSaveGameFile = savegame; } void OMW::Engine::setRandomSeed(unsigned int seed) { mRandomSeed = seed; } openmw-openmw-0.49.0/apps/openmw/engine.hpp000066400000000000000000000153341503074453300206540ustar00rootroot00000000000000#ifndef ENGINE_H #define ENGINE_H #include #include #include #include #include #include #include #include #include #include "mwbase/environment.hpp" namespace Resource { class ResourceSystem; } namespace SceneUtil { class WorkQueue; class AsyncScreenCaptureOperation; class UnrefQueue; } namespace VFS { class Manager; } namespace Compiler { class Context; } namespace MWLua { class LuaManager; class Worker; } namespace Stereo { class Manager; } namespace Files { struct ConfigurationManager; } namespace osgViewer { class ScreenCaptureHandler; } namespace SceneUtil { class SelectDepthFormatOperation; namespace Color { class SelectColorFormatOperation; } } namespace MWState { class StateManager; } namespace MWGui { class WindowManager; } namespace MWInput { class InputManager; } namespace MWSound { class SoundManager; } namespace MWWorld { class World; } namespace MWScript { class ScriptManager; } namespace MWMechanics { class MechanicsManager; } namespace MWDialogue { class DialogueManager; } namespace MWDialogue { class Journal; } namespace l10n { class Manager; } struct SDL_Window; 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; bool mExportFonts; unsigned int mRandomSeed; Debug::Level mMaxRecastLogLevel = Debug::Error; Compiler::Extensions mExtensions; std::unique_ptr mScriptContext; Files::Collections mFileCollections; Translation::Storage mTranslationDataStorage; bool mNewGame; Files::ConfigurationManager& mCfgMgr; int mGlMaxTextureImageUnits; // 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); /// Compile all dialogue scripts at startup? void setCompileAllDialogue(bool all); /// Font encoding void setEncoding(const ToUTF8::FromType& encoding); /// Enable console-only script functionality void setScriptConsoleMode(bool enabled); /// Set path for a script that is run on startup in the console. void setStartupScript(const std::filesystem::path& path); /// Override the game setting specified activation distance. void setActivationDistanceOverride(int distance); void setWarningsMode(int mode); void enableFontExport(bool exportFonts); /// Set the save game file to load after initialising the engine. void setSaveGameFile(const std::filesystem::path& savegame); void setRandomSeed(unsigned int seed); void setRecastMaxLogLevel(Debug::Level value) { mMaxRecastLogLevel = value; } }; } #endif /* ENGINE_H */ openmw-openmw-0.49.0/apps/openmw/main.cpp000066400000000000000000000206331503074453300203240ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "mwgui/debugwindow.hpp" #include "engine.hpp" #include "options.hpp" #include #if defined(_WIN32) #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 /** * \brief Parses application command line and calls \ref Cfg::ConfigurationManager * to parse configuration files. * * Results are directly written to \ref Engine class. * * \retval true - Everything goes OK * \retval false - Error */ 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 = OpenMW::makeOptionsDescription(); bpo::variables_map variables; Files::parseArgs(argc, argv, variables, desc); bpo::notify(variables); if (variables.count("help")) { Debug::getRawStdout() << desc << std::endl; return false; } if (variables.count("version")) { Debug::getRawStdout() << Version::getOpenmwVersionDescription() << std::endl; return false; } cfgMgr.processPaths(variables, std::filesystem::current_path()); cfgMgr.readConfiguration(variables, desc); 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()); Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding); engine.setEncoding(ToUTF8::calculateEncoding(encoding)); Files::PathContainer dataDirs(asPathContainer(variables["data"].as())); 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.filterOutNonExistingPaths(dataDirs); 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(); for (StringsVector::const_iterator it = archives.begin(); it != archives.end(); ++it) { engine.addArchive(*it); } 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(); 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()); 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()); engine.setWarningsMode(variables["script-warn"].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.setRandomSeed(variables["random-seed"].as()); return true; } namespace { class OSGLogHandler : public osg::NotifyHandler { void notify(osg::NotifySeverity severity, const char* msg) override { // Copy, because osg logging is not thread safe. std::string msgCopy(msg); if (msgCopy.empty()) return; 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; } std::string_view s(msgCopy); if (s.size() < 1024) Log(level) << (s.back() == '\n' ? s.substr(0, s.size() - 1) : s); else { while (!s.empty()) { size_t lineSize = 1; while (lineSize < s.size() && s[lineSize - 1] != '\n') lineSize++; Log(level) << s.substr(0, s[lineSize - 1] == '\n' ? lineSize - 1 : lineSize); s = s.substr(lineSize); } } } }; } int runApplication(int argc, char* argv[]) { Platform::init(); #ifdef __APPLE__ setenv("OSG_GL_TEXTURE_STORAGE", "OFF", 0); #endif osg::setNotifyHandler(new OSGLogHandler()); Files::ConfigurationManager cfgMgr; std::unique_ptr engine = std::make_unique(cfgMgr); engine->setRecastMaxLogLevel(Debug::getRecastMaxLogLevel()); if (parseOptions(argc, argv, *engine, cfgMgr)) { if (!Misc::checkRequiredOSGPluginsArePresent()) return 1; engine->go(); } return 0; } #ifdef ANDROID extern "C" int SDL_main(int argc, char** argv) #else int main(int argc, char** argv) #endif { return Debug::wrapApplication(&runApplication, argc, argv, "OpenMW"); } // Platform specific for Windows when there is no console built into the executable. // Windows will call the WinMain function instead of main in this case, the normal // main function is then called with the __argc and __argv parameters. #if defined(_WIN32) && !defined(_CONSOLE) int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { return main(__argc, __argv); } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/000077500000000000000000000000001503074453300201465ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwbase/dialoguemanager.hpp000066400000000000000000000071631503074453300240120ustar00rootroot00000000000000#ifndef GAME_MWBASE_DIALOGUEMANAGER_H #define GAME_MWBASE_DIALOGUEMANAGER_H #include #include #include #include #include #include namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; class RefId; } namespace MWWorld { class Ptr; } namespace MWBase { /// \brief Interface for dialogue manager (implemented in MWDialogue) class DialogueManager { DialogueManager(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; }; DialogueManager() {} virtual void clear() = 0; virtual ~DialogueManager() {} virtual bool isInChoice() const = 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 void addTopic(const ESM::RefId& topic) = 0; virtual void addChoice(std::string_view text, int choice) = 0; virtual const std::vector>& getChoices() const = 0; virtual bool isGoodbye() const = 0; virtual void goodbye() = 0; virtual bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) = 0; virtual void keywordSelected(std::string_view keyword, ResponseCallback* callback) = 0; virtual void goodbyeSelected() = 0; virtual void questionAnswered(int answer, ResponseCallback* callback) = 0; enum TopicType { Specific = 1, Exhausted = 2 }; 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 bool checkServiceRefused(ResponseCallback* callback, ServiceType service = ServiceType::Any) = 0; virtual void persuade(int type, ResponseCallback* callback) = 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 void write(ESM::ESMWriter& writer, Loading::Listener& progress) const = 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; /// Set faction1's opinion of faction2. virtual void setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) = 0; /// @return faction1's opinion of faction2 virtual int getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/environment.cpp000066400000000000000000000004641503074453300232220ustar00rootroot00000000000000#include "environment.hpp" #include #include MWBase::Environment* MWBase::Environment::sThis = nullptr; MWBase::Environment::Environment() { assert(sThis == nullptr); sThis = this; } MWBase::Environment::~Environment() { sThis = nullptr; } openmw-openmw-0.49.0/apps/openmw/mwbase/environment.hpp000066400000000000000000000111731503074453300232260ustar00rootroot00000000000000#ifndef GAME_BASE_ENVIRONMENT_H #define GAME_BASE_ENVIRONMENT_H #include #include namespace Resource { class ResourceSystem; } namespace l10n { class Manager; } namespace MWWorld { class ESMStore; class WorldModel; class Scene; } namespace MWBase { class World; class ScriptManager; class DialogueManager; class Journal; class SoundManager; class MechanicsManager; 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. /// class Environment { 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(); ~Environment(); Environment(const Environment&) = delete; Environment& operator=(const Environment&) = delete; 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; } void setSoundManager(SoundManager& value) { mSoundManager = &value; } void setScriptManager(ScriptManager& value) { mScriptManager = &value; } void setWindowManager(WindowManager& value) { mWindowManager = &value; } void setMechanicsManager(MechanicsManager& value) { mMechanicsManager = &value; } void setDialogueManager(DialogueManager& value) { mDialogueManager = &value; } void setJournal(Journal& value) { mJournal = &value; } void setInputManager(InputManager& value) { mInputManager = &value; } void setStateManager(StateManager& value) { mStateManager = &value; } void setLuaManager(LuaManager& value) { mLuaManager = &value; } void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; } void setL10nManager(l10n::Manager& value) { mL10nManager = &value; } Misc::NotNullPtr getWorld() const { return mWorld; } Misc::NotNullPtr getWorldModel() const { return mWorldModel; } Misc::NotNullPtr getWorldScene() const { return mWorldScene; } Misc::NotNullPtr getESMStore() const { return mESMStore; } Misc::NotNullPtr getSoundManager() const { return mSoundManager; } Misc::NotNullPtr getScriptManager() const { return mScriptManager; } Misc::NotNullPtr getWindowManager() const { return mWindowManager; } Misc::NotNullPtr getMechanicsManager() const { return mMechanicsManager; } Misc::NotNullPtr getDialogueManager() const { return mDialogueManager; } Misc::NotNullPtr getJournal() const { return mJournal; } Misc::NotNullPtr getInputManager() const { return mInputManager; } Misc::NotNullPtr getStateManager() const { return mStateManager; } Misc::NotNullPtr getLuaManager() const { return mLuaManager; } Misc::NotNullPtr getResourceSystem() const { return mResourceSystem; } Misc::NotNullPtr getL10nManager() const { return mL10nManager; } float getFrameRateLimit() const { return mFrameRateLimit; } void setFrameRateLimit(float value) { mFrameRateLimit = value; } float getFrameDuration() const { return mFrameDuration; } void setFrameDuration(float value) { mFrameDuration = value; } /// Return instance of this class. static const Environment& get() { assert(sThis != nullptr); return *sThis; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/inputmanager.hpp000066400000000000000000000063561503074453300233630ustar00rootroot00000000000000#ifndef GAME_MWBASE_INPUTMANAGER_H #define GAME_MWBASE_INPUTMANAGER_H #include #include #include #include #include namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWBase { /// \brief Interface for input manager (implemented in MWInput) class InputManager { InputManager(const InputManager&); ///< not implemented InputManager& operator=(const InputManager&); ///< not implemented public: InputManager() {} /// Clear all savegame-specific data virtual void clear() = 0; virtual ~InputManager() {} virtual void update(float dt, bool disableControls, bool disableEvents = false) = 0; virtual void changeInputMode(bool guiMode) = 0; virtual void processChangedSettings(const std::set>& changed) = 0; virtual void setDragDrop(bool dragDrop) = 0; virtual bool isGamepadGuiCursorEnabled() = 0; virtual void setGamepadGuiCursorEnabled(bool enabled) = 0; virtual void toggleControlSwitch(std::string_view sw, bool value) = 0; virtual bool getControlSwitch(std::string_view sw) = 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 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; /// 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; 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 bool isIdle() const = 0; virtual void executeAction(int action) = 0; virtual bool controlsDisabled() = 0; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/journal.hpp000066400000000000000000000066721503074453300223440ustar00rootroot00000000000000#ifndef GAME_MWBASE_JOURNAL_H #define GAME_MWBASE_JOURNAL_H #include #include #include #include #include #include "../mwdialogue/journalentry.hpp" #include "../mwdialogue/quest.hpp" #include "../mwdialogue/topic.hpp" namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; } 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 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: Journal() {} virtual void clear() = 0; 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 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 ESM::RefId& id, int index) = 0; ///< Set the journal index without adding an entry. virtual int getJournalIndex(const ESM::RefId& id) const = 0; ///< Get the journal index. 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 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 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 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 topicEnd() const = 0; ///< Iterator pointing past the last topic. virtual int countSavedGameRecords() const = 0; virtual void write(ESM::ESMWriter& writer, Loading::Listener& progress) const = 0; virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/luamanager.hpp000066400000000000000000000130141503074453300227720ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwbase/mechanicsmanager.hpp000066400000000000000000000336261503074453300241560ustar00rootroot00000000000000#ifndef GAME_MWBASE_MECHANICSMANAGER_H #define GAME_MWBASE_MECHANICSMANAGER_H #include #include #include #include #include #include #include "../mwmechanics/greetingstate.hpp" #include "../mwrender/animationpriority.hpp" #include "../mwworld/ptr.hpp" namespace osg { class Stats; class Vec3f; } namespace ESM { struct Class; class RefId; class ESMReader; class ESMWriter; } namespace MWWorld { class Ptr; class CellStore; class CellRef; } namespace Loading { class Listener; } 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, 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, std::string_view 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 write(ESM::ESMWriter& writer, Loading::Listener& listener) const = 0; virtual void readRecord(ESM::ESMReader& reader, uint32_t type) = 0; virtual void clear() = 0; virtual bool isAggressive(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; /// Resurrects the player if necessary virtual void resurrect(const MWWorld::Ptr& ptr) = 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 void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) = 0; virtual void processChangedSettings(const std::set>& settings) = 0; virtual void notifyDied(const MWWorld::Ptr& actor) = 0; virtual bool onOpen(const MWWorld::Ptr& ptr) = 0; virtual void onClose(const MWWorld::Ptr& ptr) = 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 void confiscateStolenItems(const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) = 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; /// Has the player stolen this item from the given owner? virtual bool isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) = 0; virtual bool isBoundItem(const MWWorld::Ptr& item) = 0; virtual bool isAllowedToUse(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, MWWorld::Ptr& victim) = 0; /// Turn actor into werewolf or normal form. virtual void setWerewolf(const MWWorld::Ptr& actor, bool werewolf) = 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 cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 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 reportStats(unsigned int frameNumber, osg::Stats& stats) const = 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/rotationflags.hpp000066400000000000000000000004601503074453300235330ustar00rootroot00000000000000#ifndef GAME_MWBASE_ROTATIONFLAGS_H #define GAME_MWBASE_ROTATIONFLAGS_H namespace MWBase { using RotationFlags = unsigned short; enum RotationFlag : RotationFlags { RotationFlag_none = 0, RotationFlag_adjust = 1, RotationFlag_inverseOrder = 1 << 1, }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/scriptmanager.hpp000066400000000000000000000025771503074453300235310ustar00rootroot00000000000000#ifndef GAME_MWBASE_SCRIPTMANAGER_H #define GAME_MWBASE_SCRIPTMANAGER_H #include namespace Interpreter { class Context; } namespace ESM { class RefId; } namespace Compiler { class Extensions; class Locals; } namespace MWScript { class GlobalScripts; } namespace MWBase { /// \brief Interface for script manager (implemented in MWScript) class ScriptManager { ScriptManager(const ScriptManager&); ///< not implemented ScriptManager& operator=(const ScriptManager&); ///< not implemented public: ScriptManager() {} virtual ~ScriptManager() {} 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 compile(const ESM::RefId& name) = 0; ///< Compile script with the given namen /// \return 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 MWScript::GlobalScripts& getGlobalScripts() = 0; virtual const Compiler::Extensions& getExtensions() const = 0; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/soundmanager.hpp000066400000000000000000000234361503074453300233520ustar00rootroot00000000000000#ifndef GAME_MWBASE_SOUNDMANAGER_H #define GAME_MWBASE_SOUNDMANAGER_H #include #include #include #include #include #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 enum BlockerType { VideoPlayback, 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. */ 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); } } namespace MWBase { using Sound = MWSound::Sound; using SoundStream = MWSound::Stream; /// \brief Interface for sound manager (implemented in MWSound) class SoundManager { 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; virtual bool isEnabled() const = 0; ///< Returns true if sound system is enabled virtual void stopMusic() = 0; ///< Stops music if it's playing virtual MWSound::MusicType getMusicType() const = 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 bool isMusicPlaying() = 0; ///< Returns true if music is playing 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 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 bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const = 0; ///< Is actor not speaking? virtual bool sayDone(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const = 0; ///< For scripting backward compatibility virtual void stopSay(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) = 0; ///< Stop an actor speaking 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 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 void stopTrack(SoundStream* stream) = 0; ///< Stop the given audio track from playing 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* 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 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 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 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* 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 void stopSound(Sound* sound) = 0; ///< Stop the given sound from playing virtual void stopSound3D(const MWWorld::ConstPtr& reference, const ESM::RefId& soundId) = 0; ///< Stop the given object from playing the given sound. 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) = 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 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 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, 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 resumeSounds(MWSound::BlockerType blocker) = 0; ///< Resumes all previously paused sounds. virtual void pausePlayback() = 0; virtual void resumePlayback() = 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; void setSimulationTimeScale(float scale) { mSimulationTimeScale = scale; } float getSimulationTimeScale() const { return mSimulationTimeScale; } virtual void clear() = 0; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/statemanager.hpp000066400000000000000000000053361503074453300233410ustar00rootroot00000000000000#ifndef GAME_MWSTATE_STATEMANAGER_H #define GAME_MWSTATE_STATEMANAGER_H #include #include #include namespace MWState { struct Slot; class Character; } namespace MWBase { /// \brief Interface for game state manager (implemented in MWState) class StateManager { public: enum State { State_NoGame, State_Ended, State_Running }; typedef std::list::const_iterator CharacterIterator; private: StateManager(const StateManager&); ///< not implemented StateManager& operator=(const StateManager&); ///< not implemented public: StateManager() {} virtual ~StateManager() {} virtual void requestQuit() = 0; virtual bool hasQuitRequest() const = 0; virtual void askLoadRecent() = 0; virtual void requestNewGame() = 0; virtual void requestLoad(const std::filesystem::path& filepath) = 0; virtual State getState() const = 0; virtual void newGame(bool bypass = false) = 0; ///< Start a new game. /// /// \param bypass Skip new game mechanics. virtual void resumeGame() = 0; virtual void deleteGame(const MWState::Character* character, const MWState::Slot* slot) = 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 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 loadGame(const MWState::Character* character, const std::filesystem::path& filepath) = 0; ///< Load a saved game file belonging to the given character. /// Simple saver, writes over the file if already existing /** Used for quick save and autosave **/ virtual void quickSave(std::string = "Quicksave") = 0; /// 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/windowmanager.hpp000066400000000000000000000337271503074453300235350ustar00rootroot00000000000000#ifndef GAME_MWBASE_WINDOWMANAGER_H #define GAME_MWBASE_WINDOWMANAGER_H #include #include #include #include #include #include #include #include #include "../mwgui/mode.hpp" #include namespace ESM { class RefId; } namespace Loading { class Listener; } namespace Translation { class Storage; } namespace MyGUI { class Gui; class Widget; class UString; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWMechanics { class AttributeValue; template class DynamicStat; class SkillValue; } namespace MWWorld { class CellStore; class Ptr; } namespace MWGui { class Layout; class Console; class SpellWindow; class TradeWindow; class TravelWindow; class SpellBuyingWindow; class ConfirmationDialog; class CountDialog; class ScrollWindow; class BookWindow; class InventoryWindow; class ContainerWindow; class DialogueWindow; class WindowModal; class JailScreen; class MessageBox; class PostProcessorHud; class SettingsWindow; enum ShowInDialogueMode { ShowInDialogueMode_IfPossible, ShowInDialogueMode_Only, ShowInDialogueMode_Never }; struct TextColours; } namespace SFO { class CursorManager; } namespace MWBase { /// \brief Interface for widnow manager (implemented in MWGui) class WindowManager : public SDLUtil::WindowListener { WindowManager(const WindowManager&); ///< not implemented WindowManager& operator=(const WindowManager&); ///< not implemented public: typedef std::vector SkillList; WindowManager() {} virtual ~WindowManager() {} /// @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; virtual void setNewGame(bool newgame) = 0; 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; virtual void removeGuiMode(MWGui::GuiMode mode) = 0; ///< can be anywhere in the stack virtual void goToJail(int days) = 0; virtual void updatePlayer() = 0; virtual MWGui::GuiMode getMode() const = 0; virtual bool containsMode(MWGui::GuiMode) const = 0; virtual bool isGuiMode() const = 0; virtual bool isConsoleMode() const = 0; virtual bool isPostProcessorHudVisible() const = 0; virtual bool isSettingsWindowVisible() const = 0; virtual bool isInteractiveMessageBoxActive() const = 0; virtual void toggleVisible(MWGui::GuiWindow wnd) = 0; virtual void forceHide(MWGui::GuiWindow wnd) = 0; virtual void unsetForceHide(MWGui::GuiWindow wnd) = 0; /// Disallow all inventory mode windows virtual void disallowAll() = 0; /// Allow one or more windows virtual void allow(MWGui::GuiWindow wnd) = 0; virtual bool isAllowed(MWGui::GuiWindow wnd) const = 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; /// Make the player use an item, while updating GUI state accordingly virtual void useItem(const MWWorld::Ptr& item, bool force = false) = 0; virtual void updateSpellWindow() = 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; 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; /// 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 changeCell(const MWWorld::CellStore* cell) = 0; ///< change the active cell virtual void setFocusObject(const MWWorld::Ptr& focus) = 0; virtual void setFocusObjectScreenCoords(float x, float y) = 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 float getScalingFactor() const = 0; virtual bool toggleFogOfWar() = 0; virtual bool toggleFullHelp() = 0; ///< show extra info in item tooltips (owner, script) virtual bool getFullHelp() const = 0; /// sets the visibility of the drowning bar virtual void setDrowningBarVisibility(bool visible) = 0; /// sets the visibility of the hud health/magicka/stamina bars virtual void setHMSVisibility(bool visible) = 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; /// 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 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; virtual void showCrosshair(bool show) = 0; virtual bool setHudVisibility(bool show) = 0; virtual bool isHudVisible() const = 0; virtual void disallowMouse() = 0; virtual void allowMouse() = 0; virtual void notifyInputActionBound() = 0; virtual void addVisitedLocation(const std::string& name, int x, int y) = 0; /// Hides dialog and schedules dialog to be deleted. virtual void removeDialog(std::unique_ptr&& dialog) = 0; /// Gracefully attempts to exit the topmost GUI mode /** No guarantee of actually closing the window **/ virtual void exitCurrentGuiMode() = 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; /// returns the index of the pressed button or -1 if no button was pressed /// (->MessageBoxmanager->InteractiveMessageBox) virtual int readPressedButton() = 0; virtual void updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) = 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; virtual void processChangedSettings(const std::set>& changed) = 0; virtual void executeInConsole(const std::filesystem::path& path) = 0; virtual void enableRest() = 0; virtual bool getRestEnabled() = 0; virtual bool getJournalAllowed() = 0; virtual bool getPlayerSleeping() = 0; virtual void wakeUpPlayer() = 0; virtual void showSoulgemDialog(MWWorld::Ptr item) = 0; virtual void changePointer(const std::string& name) = 0; virtual void setEnemy(const MWWorld::Ptr& enemy) = 0; virtual std::size_t getMessagesCount() const = 0; virtual const Translation::Storage& getTranslationDataStorage() const = 0; /// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this. virtual void setKeyFocusWidget(MyGUI::Widget* widget) = 0; virtual Loading::Listener* getLoadingScreen() = 0; /// Should the cursor be visible? virtual bool getCursorVisible() = 0; /// Clear all savegame-specific data virtual void clear() = 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; /// Does the current stack of GUI-windows permit saving? virtual bool isSavingAllowed() const = 0; /// Send exit command to active Modal window virtual void exitCurrentModal() = 0; /// Sets the current Modal /** Used to send exit command to active Modal when Esc is pressed **/ virtual void addCurrentModal(MWGui::WindowModal* input) = 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 pinWindow(MWGui::GuiWindow window) = 0; virtual void toggleMaximized(MWGui::Layout* layout) = 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 activateHitOverlay(bool interrupt = true) = 0; virtual void setWerewolfOverlay(bool set) = 0; virtual void toggleConsole() = 0; virtual void toggleDebugWindow() = 0; virtual void togglePostProcessorHud() = 0; virtual void toggleSettingsWindow() = 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 playSound(const ESM::RefId& soundId, float volume = 1.f, float pitch = 1.f) = 0; virtual void addCell(MWWorld::CellStore* cell) = 0; virtual void removeCell(MWWorld::CellStore* cell) = 0; virtual void writeFog(MWWorld::CellStore* cell) = 0; virtual const MWGui::TextColours& getTextColours() = 0; virtual bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) = 0; virtual bool injectKeyRelease(MyGUI::KeyCode key) = 0; void windowVisibilityChange(bool visible) override = 0; void windowResized(int x, int y) override = 0; void windowClosed() override = 0; virtual bool isWindowVisible() const = 0; virtual void watchActor(const MWWorld::Ptr& ptr) = 0; virtual MWWorld::Ptr getWatchedActor() const = 0; virtual const std::string& getVersionDescription() const = 0; virtual void onDeleteCustomData(const MWWorld::Ptr& ptr) = 0; virtual void forceLootMode(const MWWorld::Ptr& ptr) = 0; virtual void asyncPrepareSaveMap() = 0; /// Sets the cull masks for all applicable views virtual void setCullMask(uint32_t mask) = 0; /// Same as viewer->getCamera()->getCullMask(), provided for consistency. virtual uint32_t getCullMask() = 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwbase/world.hpp000066400000000000000000000574631503074453300220250ustar00rootroot00000000000000#ifndef GAME_MWBASE_WORLD_H #define GAME_MWBASE_WORLD_H #include "rotationflags.hpp" #include #include #include #include #include #include #include #include "../mwworld/doorstate.hpp" #include "../mwworld/globalvariablename.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/spellcaststate.hpp" #include "../mwrender/rendermode.hpp" namespace osg { class Vec3f; class Matrixf; class Quat; class Image; class Stats; } namespace Loading { class Listener; } namespace ESM { class ESMReader; class ESMWriter; struct Position; struct Cell; struct Class; struct Container; struct Creature; struct Potion; struct Spell; struct NPC; struct Armor; struct Weapon; struct Clothing; struct Enchantment; struct Book; struct EffectList; 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 { struct Movement; } namespace DetourNavigator { struct Navigator; struct AgentBounds; } namespace MWWorld { class CellStore; class Player; class LocalScripts; class TimeStamp; class ESMStore; class RefData; class Cell; class DateTimeManager; typedef std::vector> PtrMovementList; } 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::RefId dest; }; World() {} virtual ~World() {} virtual void setRandomSeed(uint32_t seed) = 0; ///< \param seed The seed used when starting a new game. 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) = 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 MWWorld::Player& getPlayer() = 0; virtual MWWorld::Ptr getPlayerPtr() = 0; virtual MWWorld::ConstPtr getPlayerConstPtr() const = 0; virtual MWWorld::ESMStore& getStore() = 0; virtual const MWWorld::ESMStore& getStore() const = 0; virtual const std::vector& getESMVersions() const = 0; virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual bool isCellExterior() const = 0; virtual bool isCellQuasiExterior() 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 setGlobalInt(MWWorld::GlobalVariableName name, int value) = 0; ///< Set value independently from real type. virtual void setGlobalFloat(MWWorld::GlobalVariableName name, float value) = 0; ///< Set value independently from real type. virtual int getGlobalInt(MWWorld::GlobalVariableName name) const = 0; ///< Get value independently from real type. virtual float getGlobalFloat(MWWorld::GlobalVariableName name) const = 0; ///< Get value independently from real type. virtual char getGlobalVariableType(MWWorld::GlobalVariableName name) const = 0; ///< Return ' ', if there is no global variable with this name. 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 void removeRefScript(const MWWorld::CellRef* ref) = 0; //< Remove the script attached to ref from mLocalScripts 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. 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 MWWorld::Ptr searchPtrViaActorId(int actorId) = 0; ///< Search is limited to the active cells. 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 enable(const MWWorld::Ptr& ptr) = 0; virtual void disable(const MWWorld::Ptr& ptr) = 0; virtual void advanceTime(double hours, bool incremental = false) = 0; ///< Advance in-game time. virtual MWWorld::TimeStamp getTimeStamp() const = 0; ///< Return current in-game time and number of day since new game start. virtual bool toggleSky() = 0; ///< \return Resulting mode virtual void changeWeather(const ESM::RefId& region, const unsigned int id) = 0; virtual int getCurrentWeather() const = 0; virtual int getNextWeather() const = 0; virtual float getWeatherTransition() const = 0; virtual unsigned int getNightDayMode() const = 0; virtual int getMasserPhase() const = 0; virtual int getSecundaPhase() const = 0; virtual void setMoonColour(bool red) = 0; virtual void modRegion(const ESM::RefId& regionid, const std::vector& chances) = 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 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 MWWorld::Ptr getFacedObject() = 0; ///< Return pointer to the object the player is looking at, if it is within activation range virtual float getDistanceToFacedObject() = 0; virtual float getMaxActivationDistance() const = 0; 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 void fixPosition() = 0; ///< Attempt to fix position so that the player is not stuck inside the geometry. /// @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 MWWorld::Ptr moveObject( const MWWorld::Ptr& ptr, const osg::Vec3f& position, bool movePhysics = true, bool moveToActive = false) = 0; ///< @return an updated Ptr in case the Ptr's cell changes virtual MWWorld::Ptr moveObject(const MWWorld::Ptr& ptr, MWWorld::CellStore* newCell, const osg::Vec3f& position, bool movePhysics = true, bool keepActive = false) = 0; ///< @return an updated Ptr virtual MWWorld::Ptr moveObjectBy(const MWWorld::Ptr& ptr, const osg::Vec3f& vec, bool moveToActive) = 0; ///< @return an updated Ptr virtual void scaleObject(const MWWorld::Ptr& ptr, float scale, bool force = false) = 0; virtual void rotateObject( const MWWorld::Ptr& ptr, const osg::Vec3f& rot, RotationFlags flags = RotationFlag_inverseOrder) = 0; virtual MWWorld::Ptr placeObject( const MWWorld::ConstPtr& ptr, MWWorld::CellStore* cell, const ESM::Position& pos) = 0; ///< Place an object. Makes a copy of the Ptr. virtual MWWorld::Ptr safePlaceObject(const MWWorld::ConstPtr& ptr, const MWWorld::ConstPtr& referenceObject, MWWorld::CellStore* referenceCell, int direction, float distance) = 0; ///< 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 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 void updateAnimatedCollisionShape(const MWWorld::Ptr& ptr) = 0; virtual const MWPhysics::RayCastingInterface* getRayCasting() const = 0; virtual bool castRenderingRay(MWPhysics::RayCastingResult& res, const osg::Vec3f& from, const osg::Vec3f& to, bool ignorePlayer, bool ignoreActors, std::span ignoreList = {}) = 0; virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool internal, bool external) = 0; virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 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; /// Returns true if levitation spell effect is allowed. virtual bool isLevitationEnabled() const = 0; virtual bool getGodModeState() const = 0; virtual bool toggleGodMode() = 0; virtual bool toggleScripts() = 0; virtual bool getScriptsEnabled() 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 void castSpell(const MWWorld::Ptr& actor, bool scriptedSpell = false) = 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; virtual void applyLoopingParticles(const MWWorld::Ptr& ptr) const = 0; virtual const std::vector& getContentFiles() const = 0; virtual void breakInvisibility(const MWWorld::Ptr& actor) = 0; // Allow NPCs to use torches? virtual bool useTorches() const = 0; virtual float getSunVisibility() const = 0; virtual float getSunPercentage() const = 0; virtual bool findInteriorPositionInWorldSpace(const MWWorld::CellStore* cell, osg::Vec3f& result) = 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; 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; /// 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; /// Moves all stolen items from \a ptr to the closest evidence chest. virtual void confiscateStolenItems(const MWWorld::Ptr& ptr) = 0; virtual void goToJail() = 0; /// Spawn a random creature from a levelled list next to the player virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0; /// Spawn a blood effect for \a ptr at \a worldPosition virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) = 0; /// @see MWWorld::WeatherManager::isInStorm virtual bool isInStorm() const = 0; /// @see MWWorld::WeatherManager::getStormDirection virtual osg::Vec3f getStormDirection() const = 0; /// Resets all actors in the current active cells to their original location within that cell. virtual void resetActors() = 0; virtual bool isWalkingOnWater(const MWWorld::ConstPtr& actor) 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; virtual void addContainerScripts(const MWWorld::Ptr& reference, MWWorld::CellStore* cell) = 0; virtual void removeContainerScripts(const MWWorld::Ptr& reference) = 0; virtual bool isPlayerInJail() const = 0; virtual void rest(double hours) = 0; virtual void setPlayerTraveling(bool traveling) = 0; virtual bool isPlayerTraveling() const = 0; virtual void rotateWorldObject(const MWWorld::Ptr& ptr, const osg::Quat& rotate) = 0; /// Return terrain height at \a worldPos position. virtual float getTerrainHeightAt(const osg::Vec3f& worldPos, ESM::RefId worldspace) 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; /// 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; /// Preload VFX associated with this effect list virtual void preloadEffects(const ESM::EffectList* effectList) = 0; virtual DetourNavigator::Navigator* getNavigator() 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; virtual void removeActorPath(const MWWorld::ConstPtr& actor) const = 0; virtual void setNavMeshNumberToRender(const std::size_t value) = 0; virtual DetourNavigator::AgentBounds getPathfindingAgentBounds(const MWWorld::ConstPtr& actor) const = 0; virtual bool hasCollisionWithDoor( const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, std::span ignore, std::vector* occupyingActors = nullptr) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual std::vector getAll(const ESM::RefId& id) = 0; virtual Misc::Rng::Generator& getPrng() = 0; virtual MWRender::RenderingManager* getRenderingManager() = 0; virtual MWRender::PostProcessor* getPostProcessor() = 0; virtual MWWorld::DateTimeManager* getTimeManager() = 0; virtual void setActorActive(const MWWorld::Ptr& ptr, bool value) = 0; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/000077500000000000000000000000001503074453300203415ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwclass/activator.cpp000066400000000000000000000166321503074453300230510ustar00rootroot00000000000000#include "activator.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/nullaction.hpp" #include "../mwworld/ptr.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwrender/vismask.hpp" #include "../mwgui/tooltips.hpp" #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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Activator::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { insertObjectPhysics(ptr, model, rotation, physics); } void Activator::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } std::string_view Activator::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } bool Activator::isActivator() const { return true; } bool Activator::useAnim() const { return true; } std::string_view Activator::getName(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mName; } ESM::RefId Activator::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } bool Activator::hasToolTip(const MWWorld::ConstPtr& ptr) const { return !getName(ptr).empty(); } MWGui::ToolTipInfo Activator::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; std::string_view name = getName(ptr); info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)) + MWGui::ToolTips::getCountString(count); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) { info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } return info; } std::unique_ptr Activator::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if (sound) action->setSound(sound->mId); return action; } return std::make_unique(); } MWWorld::Ptr Activator::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } ESM::RefId Activator::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { // 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()) { if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, iter.mModel)) { creatureId = !iter.mOriginal.empty() ? &iter.mOriginal : &iter.mId; break; } } int type = getSndGenTypeFromName(name); std::vector fallbacksounds; 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) { 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(), prng)]->mSound; if (!fallbacksounds.empty()) 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) if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); if (!fallbacksounds.empty()) return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; } return ESM::RefId(); } int Activator::getSndGenTypeFromName(std::string_view name) { if (name == "left") return ESM::SoundGenerator::LeftFoot; if (name == "right") return ESM::SoundGenerator::RightFoot; if (name == "swimleft") return ESM::SoundGenerator::SwimLeft; if (name == "swimright") return ESM::SoundGenerator::SwimRight; if (name == "moan") return ESM::SoundGenerator::Moan; if (name == "roar") return ESM::SoundGenerator::Roar; if (name == "scream") return ESM::SoundGenerator::Scream; if (name == "land") return ESM::SoundGenerator::Land; throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } } openmw-openmw-0.49.0/apps/openmw/mwclass/activator.hpp000066400000000000000000000042431503074453300230510ustar00rootroot00000000000000#ifndef GAME_MWCLASS_ACTIVATOR_H #define GAME_MWCLASS_ACTIVATOR_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Activator final : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Activator(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; static int getSndGenTypeFromName(std::string_view name); 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, 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_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) 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 std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool useAnim() const override; ///< Whether or not to use animated variant of model (default false) bool isActivator() const override; ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/actor.cpp000066400000000000000000000063351503074453300221640ustar00rootroot00000000000000#include "actor.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/movement.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/worldmodel.hpp" namespace MWClass { 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, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { physics.addActor(ptr, VFS::Path::toNormalized(model)); if (getCreatureStats(ptr).isDead() && getCreatureStats(ptr).isDeathAnimationFinished()) MWBase::Environment::get().getWorld()->enableActorCollision(ptr, false); } bool Actor::useAnim() const { return true; } osg::Vec3f Actor::getRotationVector(const MWWorld::Ptr& ptr) const { 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; movement.mRotation[2] = 0.0f; return vec; } float Actor::getEncumbrance(const MWWorld::Ptr& ptr) const { float weight = getContainerStore(ptr).getWeight(); const MWMechanics::MagicEffects& effects = getCreatureStats(ptr).getMagicEffects(); weight -= effects.getOrDefault(MWMechanics::EffectKey(ESM::MagicEffect::Feather)).getMagnitude(); if (ptr != MWMechanics::getPlayer() || !MWBase::Environment::get().getWorld()->getGodModeState()) weight += effects.getOrDefault(MWMechanics::EffectKey(ESM::MagicEffect::Burden)).getMagnitude(); return (weight < 0) ? 0.0f : weight; } bool Actor::allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return false; } bool Actor::isActor() const { return true; } float Actor::getCurrentSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::Movement& movementSettings = ptr.getClass().getMovementSettings(ptr); float moveSpeed = this->getMaxSpeed(ptr) * movementSettings.mSpeedFactor; if (movementSettings.mIsStrafing) 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; } } openmw-openmw-0.49.0/apps/openmw/mwclass/actor.hpp000066400000000000000000000046241503074453300221700ustar00rootroot00000000000000#ifndef GAME_MWCLASS_MOBILE_H #define GAME_MWCLASS_MOBILE_H #include "../mwworld/class.hpp" #include "../mwmechanics/magiceffects.hpp" #include #include namespace ESM { struct GameSetting; } namespace MWClass { /// \brief Class holding functionality common to Creature and NPC class Actor : public MWWorld::Class { protected: explicit Actor(unsigned type) : Class(type) { } 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: ~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, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool useAnim() const override; osg::Vec3f getRotationVector(const MWWorld::Ptr& ptr) const override; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. 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. bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; ///< Return whether this class of object can be activated with telekinesis bool isActor() const override; /// 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&) = delete; Actor& operator=(const Actor&) = delete; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/apparatus.cpp000066400000000000000000000102711503074453300230460ustar00rootroot00000000000000#include "apparatus.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include #include "../mwworld/actionalchemy.hpp" #include "../mwworld/cellstore.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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Apparatus::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Apparatus::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Apparatus::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Apparatus::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Apparatus::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Apparatus::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Apparatus Up"); return sound; } const ESM::RefId& Apparatus::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Apparatus Down"); return sound; } const std::string& Apparatus::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Apparatus::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } std::unique_ptr Apparatus::use(const MWWorld::Ptr& ptr, bool force) const { return std::make_unique(force); } MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Apparatus::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Apparatus) != 0; } float Apparatus::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/apparatus.hpp000066400000000000000000000044201503074453300230520ustar00rootroot00000000000000#ifndef GAME_MWCLASS_APPARATUS_H #define GAME_MWCLASS_APPARATUS_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Apparatus : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Apparatus(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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 std::string_view 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::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation 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. const ESM::RefId& 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 const 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::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/armor.cpp000066400000000000000000000337621503074453300222000ustar00rootroot00000000000000#include "armor.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/ptr.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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Armor::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Armor::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } 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 { return true; } int Armor::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mHealth; } ESM::RefId Armor::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Armor::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { 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 }, { ESM::Armor::Cuirass, MWWorld::InventoryStore::Slot_Cuirass }, { ESM::Armor::LPauldron, MWWorld::InventoryStore::Slot_LeftPauldron }, { ESM::Armor::RPauldron, MWWorld::InventoryStore::Slot_RightPauldron }, { ESM::Armor::Greaves, MWWorld::InventoryStore::Slot_Greaves }, { ESM::Armor::Boots, MWWorld::InventoryStore::Slot_Boots }, { ESM::Armor::LGauntlet, MWWorld::InventoryStore::Slot_LeftGauntlet }, { 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 } }; for (int i = 0; i < size; ++i) if (sMapping[i][0] == ref->mBase->mData.mType) { slots_.push_back(int(sMapping[i][1])); break; } return std::make_pair(slots_, false); } ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); 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::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::LGauntlet: 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; } if (typeGmst.empty()) return {}; 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) return ESM::Skill::LightArmor; 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const { 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"); if (es == ESM::Skill::LightArmor) return lightUp; else if (es == ESM::Skill::MediumArmor) return mediumUp; else return heavyUp; } const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const { 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 lightDown; else if (es == ESM::Skill::MediumArmor) return mediumDown; else return heavyDown; } const std::string& Armor::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Armor::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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_view typeText; if (ref->mBase->mData.mWeight == 0) { // no type } else { const ESM::RefId armorType = getEquipmentSkill(ptr); if (armorType == ESM::Skill::LightArmor) typeText = "#{sLight}"; else if (armorType == ESM::Skill::MediumArmor) typeText = "#{sMedium}"; else typeText = "#{sHeavy}"; } 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); 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()) { 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 = std::move(text); return info; } ESM::RefId Armor::getEnchantment(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } 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(); ESM::Armor newItem = *ref->mBase; 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 { const MWWorld::LiveCellRef* ref = ptr.get(); const ESM::RefId armorSkillType = getEquipmentSkill(ptr); float armorSkill = actor.getClass().getSkill(actor, armorSkillType); int iBaseArmorSkill = MWBase::Environment::get() .getESMStore() ->get() .find("iBaseArmorSkill") ->mValue.getInteger(); 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 { const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc); if (getItemHealth(ptr) == 0) return { 0, "#{sInventoryMessage1}" }; // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return { 0, {} }; if (npc.getClass().isNpc()) { 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().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) { 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) { // If equipping a shield, check if there's a twohanded weapon conflicting with it if (*slot == MWWorld::InventoryStore::Slot_CarriedLeft) { MWWorld::ConstContainerStoreIterator weapon = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon != invStore.end() && weapon->getType() == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef* ref = weapon->get(); if (MWMechanics::getWeaponType(ref->mBase->mData.mType)->mFlags & ESM::WeaponType::TwoHanded) return { 3, {} }; } return { 1, {} }; } } return { 1, {} }; } std::unique_ptr Armor::use(const MWWorld::Ptr& ptr, bool force) const { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Armor::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Armor::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Armor) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Armor::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/armor.hpp000066400000000000000000000077111503074453300222000ustar00rootroot00000000000000#ifndef GAME_MWCLASS_ARMOR_H #define GAME_MWCLASS_ARMOR_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Armor : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Armor(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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 std::string_view 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::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? int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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 getEquipmentSkill(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. int getValue(const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string 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::unique_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; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/bodypart.cpp000066400000000000000000000023201503074453300226660ustar00rootroot00000000000000#include "bodypart.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 { 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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view BodyPart::getName(const MWWorld::ConstPtr& ptr) const { return {}; } bool BodyPart::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } std::string_view BodyPart::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } } openmw-openmw-0.49.0/apps/openmw/mwclass/bodypart.hpp000066400000000000000000000017731503074453300227060ustar00rootroot00000000000000#ifndef GAME_MWCLASS_BODYPART_H #define GAME_MWCLASS_BODYPART_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class BodyPart : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; BodyPart(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_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::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/book.cpp000066400000000000000000000133351503074453300220040ustar00rootroot00000000000000#include "book.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/actionread.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/ptr.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" #include "../mwmechanics/npcstats.hpp" #include "classmodel.hpp" #include "nameorid.hpp" namespace MWClass { Book::Book() : MWWorld::RegisteredClass(ESM::Book::sRecordId) { } void Book::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Book::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Book::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Book::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if (sound) action->setSound(sound->mId); return action; } return std::make_unique(ptr); } ESM::RefId Book::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Book::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Book::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static auto var = ESM::RefId::stringRefId("Item Book Up"); return var; } const ESM::RefId& Book::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static auto var = ESM::RefId::stringRefId("Item Book Down"); return var; } const std::string& Book::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Book::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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 += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } ESM::RefId Book::getEnchantment(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } 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(); ESM::Book newItem = *ref->mBase; 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().getESMStore()->insert(newItem); return record->mId; } std::unique_ptr Book::use(const MWWorld::Ptr& ptr, bool force) const { return std::make_unique(ptr); } MWWorld::Ptr Book::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Book::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Book::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Books) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Book::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/book.hpp000066400000000000000000000054261503074453300220130ustar00rootroot00000000000000#ifndef GAME_MWCLASS_BOOK_H #define GAME_MWCLASS_BOOK_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Book : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Book(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation ESM::RefId 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. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string 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::unique_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; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/classes.cpp000066400000000000000000000063231503074453300225060ustar00rootroot00000000000000#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 "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 "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 "weapon.hpp" #include "esm4base.hpp" #include "esm4npc.hpp" #include "light4.hpp" namespace MWClass { void registerClasses() { Activator::registerSelf(); Creature::registerSelf(); Npc::registerSelf(); Weapon::registerSelf(); Armor::registerSelf(); Potion::registerSelf(); Apparatus::registerSelf(); Book::registerSelf(); Clothing::registerSelf(); Container::registerSelf(); Door::registerSelf(); Ingredient::registerSelf(); CreatureLevList::registerSelf(); ItemLevList::registerSelf(); Light::registerSelf(); Lockpick::registerSelf(); Miscellaneous::registerSelf(); Probe::registerSelf(); 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(); } } openmw-openmw-0.49.0/apps/openmw/mwclass/classes.hpp000066400000000000000000000002351503074453300225070ustar00rootroot00000000000000#ifndef GAME_MWCLASS_CLASSES_H #define GAME_MWCLASS_CLASSES_H namespace MWClass { void registerClasses(); ///< register all known classes } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/classmodel.hpp000066400000000000000000000006221503074453300232000ustar00rootroot00000000000000#ifndef OPENMW_MWCLASS_CLASSMODEL_H #define OPENMW_MWCLASS_CLASSMODEL_H #include "../mwworld/livecellref.hpp" #include "../mwworld/ptr.hpp" #include namespace MWClass { template std::string_view getClassModel(const MWWorld::ConstPtr& ptr) { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mModel; } } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/clothing.cpp000066400000000000000000000222611503074453300226570ustar00rootroot00000000000000#include "clothing.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Clothing::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Clothing::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Clothing::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Clothing::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Clothing::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); std::vector slots_; if (ref->mBase->mData.mType == ESM::Clothing::Ring) { 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 }, { ESM::Clothing::Belt, MWWorld::InventoryStore::Slot_Belt }, { ESM::Clothing::Robe, MWWorld::InventoryStore::Slot_Robe }, { ESM::Clothing::Pants, MWWorld::InventoryStore::Slot_Pants }, { ESM::Clothing::Shoes, MWWorld::InventoryStore::Slot_Boots }, { 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 } }; for (int i = 0; i < size; ++i) if (sMapping[i][0] == ref->mBase->mData.mType) { slots_.push_back(int(sMapping[i][1])); break; } } return std::make_pair(slots_, false); } ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); if (ref->mBase->mData.mType == ESM::Clothing::Shoes) return ESM::Skill::Unarmored; return {}; } int Clothing::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Clothing::getUpSoundId(const MWWorld::ConstPtr& ptr) const { 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 ringUp; } return clothsUp; } const ESM::RefId& Clothing::getDownSoundId(const MWWorld::ConstPtr& ptr) const { 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 ringDown; } return clothsDown; } const std::string& Clothing::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Clothing::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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 += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } ESM::RefId Clothing::getEnchantment(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } 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(); ESM::Clothing newItem = *ref->mBase; 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 { // slots that this item can be equipped in std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return { 0, {} }; if (npc.getClass().isNpc()) { 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().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) { if ((*itr).mPart == ESM::PRT_Head) return { 0, "#{sNotifyMessage13}" }; if ((*itr).mPart == ESM::PRT_LFoot || (*itr).mPart == ESM::PRT_RFoot) return { 0, "#{sNotifyMessage15}" }; } } } return { 1, {} }; } std::unique_ptr Clothing::use(const MWWorld::Ptr& ptr, bool force) const { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Clothing::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Clothing::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Clothing) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Clothing::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/clothing.hpp000066400000000000000000000067371503074453300226760ustar00rootroot00000000000000#ifndef GAME_MWCLASS_CLOTHING_H #define GAME_MWCLASS_CLOTHING_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Clothing : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Clothing(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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 getEquipmentSkill(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. int getValue(const MWWorld::ConstPtr& ptr) const override; ///< Return trade value of the object. Throws an exception, if the object can't be traded. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string 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::unique_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; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/container.cpp000066400000000000000000000274731503074453300230440ustar00rootroot00000000000000#include "container.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionharvest.hpp" #include "../mwworld/actionopen.hpp" #include "../mwworld/actiontrap.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" #include "../mwrender/animation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.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) { 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, ESM::RefId(), seed); } ContainerCustomData::ContainerCustomData(const ESM::InventoryState& inventory) { mStore.readState(inventory); } ContainerCustomData& ContainerCustomData::asContainerCustomData() { return *this; } const ContainerCustomData& ContainerCustomData::asContainerCustomData() const { return *this; } Container::Container() : MWWorld::RegisteredClass(ESM::Container::sRecordId) { } void Container::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { MWBase::Environment::get().getWorldModel()->registerPtr(ptr); MWWorld::LiveCellRef* ref = ptr.get(); // store ptr.getRefData().setCustomData(std::make_unique(*ref->mBase, ptr.getCell())); getContainerStore(ptr).setPtr(ptr); MWBase::Environment::get().getWorld()->addContainerScripts(ptr, ptr.getCell()); } } bool Container::canBeHarvested(const MWWorld::ConstPtr& ptr) const { if (!Settings::game().mGraphicHerbalism) return false; const MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation == nullptr) return false; return animation->canBeHarvested(); } void Container::respawn(const MWWorld::Ptr& ptr) const { MWWorld::LiveCellRef* ref = ptr.get(); if (ref->mBase->mFlags & ESM::Container::Respawn) { // Container was not touched, there is no need to modify its content. if (ptr.getRefData().getCustomData() == nullptr) return; MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); ptr.getRefData().setCustomData(nullptr); } } void Container::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } void Container::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { insertObjectPhysics(ptr, model, rotation, physics); } void Container::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } std::string_view Container::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } bool Container::useAnim() const { return true; } 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::make_unique(); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if (sound) action->setSound(sound->mId); return action; } MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWWorld::InventoryStore& invStore = player.getClass().getInventoryStore(player); 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); if (!keyPtr.isEmpty()) { hasKey = true; keyName = keyPtr.getClass().getName(keyPtr); } } if (isLocked && hasKey) { MWBase::Environment::get().getWindowManager()->messageBox(std::string{ keyName } + " #{sKeyUsed}"); ptr.getCellRef().unlock(); // using a key disarms the trap if (isTrapped) { 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 (canBeHarvested(ptr)) { return std::make_unique(ptr); } return std::make_unique(ptr); } else { // Activate trap std::unique_ptr action = std::make_unique(ptr.getCellRef().getTrap(), ptr); action->setSound(ESM::RefId::stringRefId("Disarm Trap Fail")); return action; } } else { std::unique_ptr action = std::make_unique(std::string_view{}, ptr); action->setSound(ESM::RefId::stringRefId("LockedChest")); return action; } } std::string_view Container::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } MWWorld::ContainerStore& Container::getContainerStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asContainerCustomData().mStore; } ESM::RefId Container::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } bool Container::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) { if (!canBeHarvested(ptr)) return true; const MWWorld::ContainerStore& store = data->asContainerCustomData().mStore; return !store.isResolved() || store.hasVisibleItems(); } return true; } MWGui::ToolTipInfo Container::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; std::string_view name = getName(ptr); info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; int lockLevel = ptr.getCellRef().getLockLevel(); 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()) { 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 = std::move(text); return info; } float Container::getCapacity(const MWWorld::Ptr& ptr) const { MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mWeight; } float Container::getEncumbrance(const MWWorld::Ptr& ptr) const { return getContainerStore(ptr).getWeight(); } bool Container::canLock(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return !(ref->mBase->mFlags & ESM::Container::Organic); } 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 { 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 { 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 { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const ContainerCustomData& customData = ptr.getRefData().getCustomData()->asContainerCustomData(); if (!customData.mStore.isResolved()) { state.mHasCustomState = false; return; } ESM::ContainerState& containerState = state.asContainerState(); customData.mStore.writeState(containerState.mInventory); } } openmw-openmw-0.49.0/apps/openmw/mwclass/container.hpp000066400000000000000000000073751503074453300230500ustar00rootroot00000000000000#ifndef GAME_MWCLASS_CONTAINER_H #define GAME_MWCLASS_CONTAINER_H #include "../mwworld/containerstore.hpp" #include "../mwworld/customdata.hpp" #include "../mwworld/registeredclass.hpp" namespace ESM { struct Container; struct InventoryState; } namespace MWClass { class ContainerCustomData : public MWWorld::TypedCustomData { MWWorld::ContainerStore mStore; public: ContainerCustomData(const ESM::Container& container, MWWorld::CellStore* cell); ContainerCustomData(const ESM::InventoryState& inventory); ContainerCustomData& asContainerCustomData() override; const ContainerCustomData& asContainerCustomData() const override; friend class Container; }; class Container : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Container(); void ensureCustomData(const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; bool canBeHarvested(const MWWorld::ConstPtr& ptr) const; 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, 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_view getName(const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation 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. MWWorld::ContainerStore& getContainerStore(const MWWorld::Ptr& ptr) const override; ///< Return container store ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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. bool canLock(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 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; std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; bool useAnim() const override; void modifyBaseInventory(const ESM::RefId& containerId, const ESM::RefId& itemId, int amount) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/creature.cpp000066400000000000000000001111701503074453300226600ustar00rootroot00000000000000#include "creature.hpp" #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/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 "../mwworld/actionopen.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/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwgui/tooltips.hpp" #include "classmodel.hpp" #include "nameorid.hpp" namespace { bool isFlagBitSet(const MWWorld::ConstPtr& ptr, ESM::Creature::Flags bitMask) { return (ptr.get()->mBase->mFlags & bitMask) != 0; } } namespace MWClass { class CreatureCustomData : public MWWorld::TypedCustomData { public: MWMechanics::CreatureStats mCreatureStats; std::unique_ptr mContainerStore; // may be InventoryStore for some creatures MWMechanics::Movement mMovement; CreatureCustomData() = default; CreatureCustomData(const CreatureCustomData& other); CreatureCustomData(CreatureCustomData&& other) = default; 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) { } Creature::Creature() : MWWorld::RegisteredClass(ESM::Creature::sRecordId) { } const Creature::GMST& Creature::getGmst() { 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"); gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); gmst.fMinFlySpeed = store.find("fMinFlySpeed"); gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); gmst.fSwimRunBase = store.find("fSwimRunBase"); gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); return gmst; }(); return staticGmst; } void Creature::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { 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(); // creature stats 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)); data->mCreatureStats.setLevel(ref->mBase->mData.mLevel); data->mCreatureStats.getAiSequence().fill(ref->mBase->mAiPackage); 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()) data->mCreatureStats.setDeathAnimationFinished(isPersistent(ptr)); // spells bool spellsInitialised = data->mCreatureStats.getSpells().setSpells(ref->mBase->mId); if (!spellsInitialised) data->mCreatureStats.getSpells().addAllToInstance(ref->mBase->mSpells.mList); // inventory bool hasInventory = hasInventoryStore(ptr); if (hasInventory) data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); data->mContainerStore->setPtr(ptr); data->mCreatureStats.setGoldPool(ref->mBase->mData.mGold); resetter.mPtr = {}; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); getContainerStore(ptr).fill(ref->mBase->mInventory, ptr.getCellRef().getRefId(), prng); if (hasInventory) getInventoryStore(ptr).autoEquip(); } } 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_view Creature::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } void Creature::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); if (customData && hasInventoryStore(ptr)) { const auto& invStore = static_cast(*customData->asCreatureCustomData().mContainerStore); for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); if (equipped != invStore.end()) { model = equipped->getClass().getModel(*equipped); if (!model.empty()) models.push_back(model); } } } } std::string_view Creature::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } MWMechanics::CreatureStats& Creature::getCreatureStats(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mCreatureStats; } bool Creature::evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const { 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::ContainerStoreIterator weaponslot = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weaponslot != inv.end() && weaponslot->getType() == ESM::Weapon::sRecordId) weapon = *weaponslot; } MWBase::World* world = MWBase::Environment::get().getWorld(); const float dist = MWMechanics::getMeleeWeaponReach(ptr, weapon); const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; // 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; 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; } MWMechanics::applyFatigueLoss(ptr, weapon, attackStrength); if (victim.isEmpty()) return; // Didn't hit anything const MWWorld::Class& othercls = victim.getClass(); MWMechanics::CreatureStats& otherstats = othercls.getCreatureStats(victim); if (otherstats.isDead()) // Can't hit dead actors return; if (!MWMechanics::isInMeleeReach(ptr, victim, MWMechanics::getMeleeWeaponReach(ptr, weapon))) return; if (!success) { victim.getClass().onHit( victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } 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; } 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.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); MWMechanics::adjustWeaponDamage(damage, weapon, ptr); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); } // Apply "On hit" enchanted weapons MWMechanics::applyOnStrikeEnchantment(ptr, victim, weapon, hitPosition); } else if (isBipedal(ptr)) { MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength); } MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; MWMechanics::diseaseContact(victim, ptr); 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 MWMechanics::DamageSourceType sourceType) const { MWMechanics::CreatureStats& stats = getCreatureStats(ptr); // 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); // 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()) { MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) && (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.setHitAttemptActorId(stats.getActorId()); } if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const ESM::RefId& script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ if (!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } if (!successful) { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) MWBase::Environment::get().getSoundManager()->playSound3D( ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); if (damage < 0.001f) damage = 0; if (damage > 0.f) { if (!attacker.isEmpty()) { // Check for knockdown 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(); 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) { damage *= damage / (damage + getArmorRating(ptr)); damage = std::max(1.f, damage); if (!attacker.isEmpty()) { damage = scaleDamage(damage, attacker, ptr); MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } MWBase::Environment::get().getSoundManager()->playSound3D( ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); MWMechanics::DynamicStat health(stats.getHealth()); health.setCurrent(health.getCurrent() - damage); stats.setHealth(health); } else { MWMechanics::DynamicStat fatigue(stats.getFatigue()); fatigue.setCurrent(fatigue.getCurrent() - damage, true); stats.setFatigue(fatigue); } } } std::unique_ptr Creature::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if (sound) action->setSound(sound->mId); return action; } const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if (stats.isDead()) { // 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::make_unique(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::make_unique(ptr); return std::make_unique(); } MWWorld::ContainerStore& Creature::getContainerStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return *ptr.getRefData().getCustomData()->asCreatureCustomData().mContainerStore; } MWWorld::InventoryStore& Creature::getInventoryStore(const MWWorld::Ptr& ptr) const { if (hasInventoryStore(ptr)) return static_cast(getContainerStore(ptr)); else throw std::runtime_error("this creature has no inventory store"); } bool Creature::hasInventoryStore(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Weapon); } ESM::RefId Creature::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } bool Creature::isEssential(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Essential); } float Creature::getMaxSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const GMST& gmst = getGmst(); const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); float moveSpeed; if (getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; else if (canFly(ptr) || (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled())) { 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)) moveSpeed = getSwimSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); return moveSpeed; } MWMechanics::Movement& Creature::getMovementSettings(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asCreatureCustomData().mMovement; } bool Creature::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) return true; const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); if (customData.mCreatureStats.isDead() && customData.mCreatureStats.isDeathAnimationFinished()) return true; const MWMechanics::AiSequence& aiSeq = customData.mCreatureStats.getAiSequence(); return !aiSeq.isInCombat() || aiSeq.isFleeing(); } MWGui::ToolTipInfo Creature::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; std::string_view name = getName(ptr); info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); if (MWBase::Environment::get().getWindowManager()->getFullHelp()) info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } float Creature::getArmorRating(const MWWorld::Ptr& ptr) const { // Equipment armor rating is deliberately ignored. return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude(); } float Creature::getCapacity(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); return stats.getAttribute(ESM::Attribute::Strength).getModified() * 5; } int Creature::getServices(const MWWorld::ConstPtr& actor) const { return actor.get()->mBase->mAiData.mServices; } bool Creature::isPersistent(const MWWorld::ConstPtr& actor) const { const MWWorld::LiveCellRef* ref = actor.get(); return (ref->mBase->mRecordFlags & ESM::FLAG_Persistent) != 0; } ESM::RefId Creature::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { int type = getSndGenTypeFromName(ptr, name); if (type < 0) return ESM::RefId(); std::vector sounds; std::vector fallbacksounds; MWWorld::LiveCellRef* ref = ptr.get(); const ESM::RefId& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal; 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() && ourId == sound->mCreature) sounds.push_back(&*sound); if (type == sound->mType && sound->mCreature.empty()) fallbacksounds.push_back(&*sound); ++sound; } if (sounds.empty()) { const std::string_view model = getModel(ptr); if (!model.empty()) { for (const ESM::Creature& creature : store.get()) { if (creature.mId != ourId && creature.mOriginal != ourId && !creature.mModel.empty() && Misc::StringUtils::ciEqual(model, creature.mModel)) { 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() && fallbackId == sound->mCreature) sounds.push_back(&*sound); ++sound; } break; } } } } auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (!sounds.empty()) return sounds[Misc::Rng::rollDice(sounds.size(), prng)]->mSound; if (!fallbacksounds.empty()) return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size(), prng)]->mSound; return ESM::RefId(); } MWWorld::Ptr Creature::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { 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 { return isFlagBitSet(ptr, ESM::Creature::Bipedal); } bool Creature::canFly(const MWWorld::ConstPtr& ptr) const { return isFlagBitSet(ptr, ESM::Creature::Flies); } 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 { return isFlagBitSet(ptr, static_cast(ESM::Creature::Walks | ESM::Creature::Bipedal)); } int Creature::getSndGenTypeFromName(const MWWorld::Ptr& ptr, std::string_view name) { if (name == "left") { 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)) return ESM::SoundGenerator::SwimLeft; if (world->isOnGround(ptr)) return ESM::SoundGenerator::LeftFoot; return -1; } if (name == "right") { 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)) return ESM::SoundGenerator::SwimRight; if (world->isOnGround(ptr)) return ESM::SoundGenerator::RightFoot; return -1; } if (name == "swimleft") return ESM::SoundGenerator::SwimLeft; if (name == "swimright") return ESM::SoundGenerator::SwimRight; if (name == "moan") return ESM::SoundGenerator::Moan; if (name == "roar") return ESM::SoundGenerator::Roar; if (name == "scream") return ESM::SoundGenerator::Scream; if (name == "land") return ESM::SoundGenerator::Land; throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } float Creature::getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const { MWWorld::LiveCellRef* ref = ptr.get(); 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"); } } int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mBloodType; } void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; const ESM::CreatureState& creatureState = state.asCreatureState(); if (!ptr.getRefData().getCustomData()) { if (creatureState.mCreatureStats.mMissingACDT) ensureCustomData(ptr); else { // Create a CustomData, but don't fill it from ESM records (not needed) auto data = std::make_unique(); if (hasInventoryStore(ptr)) data->mContainerStore = std::make_unique(); else data->mContainerStore = std::make_unique(); MWBase::Environment::get().getWorldModel()->registerPtr(ptr); data->mContainerStore->setPtr(ptr); ptr.getRefData().setCustomData(std::move(data)); } } CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); customData.mContainerStore->readState(creatureState.mInventory); bool spellsInitialised = customData.mCreatureStats.getSpells().setSpells(ptr.get()->mBase->mId); if (spellsInitialised) customData.mCreatureStats.getSpells().clear(); customData.mCreatureStats.readState(creatureState.mCreatureStats); } void Creature::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const CreatureCustomData& customData = ptr.getRefData().getCustomData()->asCreatureCustomData(); if (ptr.getCellRef().getCount() <= 0 && (!isFlagBitSet(ptr, ESM::Creature::Respawn) || !customData.mCreatureStats.isDead())) { state.mHasCustomState = false; return; } ESM::CreatureState& creatureState = state.asCreatureState(); customData.mContainerStore->writeState(creatureState.mInventory); customData.mCreatureStats.writeState(creatureState.mCreatureStats); } int Creature::getBaseGold(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mData.mGold; } void Creature::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; 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.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (isFlagBitSet(ptr, ESM::Creature::Respawn) && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { if (ptr.getCellRef().getCount() == 0) { 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().asVec3()); MWBase::Environment::get().getWorld()->rotateObject( ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } int Creature::getBaseFightRating(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mAiData.mFight; } void Creature::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool /* rendering */) const { const MWWorld::LiveCellRef* ref = ptr.get(); scale *= ref->mBase->mScale; } void Creature::setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } void Creature::modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } float Creature::getWalkSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); 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()); } float Creature::getRunSpeed(const MWWorld::Ptr& ptr) const { return getWalkSpeed(ptr); } float Creature::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); return getSwimSpeedImpl(ptr, getGmst(), mageffects, getWalkSpeed(ptr)); } } openmw-openmw-0.49.0/apps/openmw/mwclass/creature.hpp000066400000000000000000000146041503074453300226710ustar00rootroot00000000000000#ifndef GAME_MWCLASS_CREATURE_H #define GAME_MWCLASS_CREATURE_H #include "../mwworld/registeredclass.hpp" #include "actor.hpp" namespace ESM { struct GameSetting; } namespace MWClass { class Creature : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Creature(); void ensureCustomData(const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; static int getSndGenTypeFromName(const MWWorld::Ptr& ptr, std::string_view name); // 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 const GMST& getGmst(); 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_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) 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 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 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 MWMechanics::DamageSourceType sourceType) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation MWWorld::ContainerStore& getContainerStore(const MWWorld::Ptr& ptr) const override; ///< Return container store MWWorld::InventoryStore& getInventoryStore(const MWWorld::Ptr& ptr) const override; ///< Return inventory store bool hasInventoryStore(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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 getArmorRating(const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor 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; bool isPersistent(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; MWMechanics::Movement& getMovementSettings(const MWWorld::Ptr& ptr) const override; ///< Return desired movement. float getMaxSpeed(const MWWorld::Ptr& ptr) const override; std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; 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; float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override; /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) int getBloodTexture(const MWWorld::ConstPtr& ptr) const override; void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override; ///< Read additional state from \a state into \a ptr. 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; void respawn(const MWWorld::Ptr& 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 setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) 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 getRunSpeed(const MWWorld::Ptr& ptr) const override; float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/creaturelevlist.cpp000066400000000000000000000154631503074453300242730ustar00rootroot00000000000000#include "creaturelevlist.hpp" #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 { public: // actorId of the creature we spawned int mSpawnActorId; bool mSpawn; // Should a new creature be spawned? CreatureLevListCustomData& asCreatureLevListCustomData() override { return *this; } const CreatureLevListCustomData& asCreatureLevListCustomData() const override { return *this; } }; CreatureLevList::CreatureLevList() : MWWorld::RegisteredClass(ESM::CreatureLevList::sRecordId) { } MWWorld::Ptr CreatureLevList::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } void CreatureLevList::adjustPosition(const MWWorld::Ptr& ptr, bool force) const { if (ptr.getRefData().getCustomData() == nullptr) return; CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); MWWorld::Ptr creature = (customData.mSpawnActorId == -1) ? MWWorld::Ptr() : MWBase::Environment::get().getWorld()->searchPtrViaActorId(customData.mSpawnActorId); if (!creature.isEmpty()) MWBase::Environment::get().getWorld()->adjustPosition(creature, force); } std::string_view CreatureLevList::getName(const MWWorld::ConstPtr& ptr) const { return {}; } bool CreatureLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } void CreatureLevList::respawn(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); if (customData.mSpawn) return; 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.getCellRef().getCount() == 0) customData.mSpawn = true; else if (creatureStats.isDead()) { 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 = std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) customData.mSpawn = true; } } else customData.mSpawn = true; } void CreatureLevList::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); if (!customData.mSpawn) return; 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); if (!creature.isEmpty()) MWBase::Environment::get().getWorld()->deleteObject(creature); customData.mSpawnActorId = -1; } 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.getRefData().getPosition()); customData.mSpawnActorId = placed.getClass().getCreatureStats(placed).getActorId(); customData.mSpawn = false; } else customData.mSpawn = false; } void CreatureLevList::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { std::unique_ptr data = std::make_unique(); data->mSpawnActorId = -1; data->mSpawn = true; ptr.getRefData().setCustomData(std::move(data)); } } void CreatureLevList::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; ensureCustomData(ptr); CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); const ESM::CreatureLevListState& levListState = state.asCreatureLevListState(); customData.mSpawnActorId = levListState.mSpawnActorId; customData.mSpawn = levListState.mSpawn; } void CreatureLevList::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const CreatureLevListCustomData& customData = ptr.getRefData().getCustomData()->asCreatureLevListCustomData(); ESM::CreatureLevListState& levListState = state.asCreatureLevListState(); levListState.mSpawnActorId = customData.mSpawnActorId; levListState.mSpawn = customData.mSpawn; } } openmw-openmw-0.49.0/apps/openmw/mwclass/creaturelevlist.hpp000066400000000000000000000027631503074453300242770ustar00rootroot00000000000000#ifndef GAME_MWCLASS_CREATURELEVLIST_H #define GAME_MWCLASS_CREATURELEVLIST_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class CreatureLevList : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; CreatureLevList(); void ensureCustomData(const MWWorld::Ptr& ptr) const; public: 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) 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 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; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; void adjustPosition(const MWWorld::Ptr& ptr, bool force) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/door.cpp000066400000000000000000000317621503074453300220210ustar00rootroot00000000000000#include "door.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/physicssystem.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/vismask.hpp" #include "../mwmechanics/actorutil.hpp" #include "classmodel.hpp" #include "nameorid.hpp" namespace MWClass { class DoorCustomData : public MWWorld::TypedCustomData { public: MWWorld::DoorState mDoorState = MWWorld::DoorState::Idle; DoorCustomData& asDoorCustomData() override { return *this; } const DoorCustomData& asDoorCustomData() const override { return *this; } }; 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); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Door::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { insertObjectPhysics(ptr, model, rotation, physics); // Resume the door's opening/closing animation if it wasn't finished if (ptr.getRefData().getCustomData()) { const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); if (customData.mDoorState != MWWorld::DoorState::Idle) { MWBase::Environment::get().getWorld()->activateDoor(ptr, customData.mDoorState); } } } void Door::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_Door); } bool Door::isDoor() const { return true; } bool Door::useAnim() const { return true; } std::string_view Door::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Door::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Door::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { MWWorld::LiveCellRef* ref = ptr.get(); const ESM::RefId& openSound = ref->mBase->mOpenSound; const ESM::RefId& closeSound = ref->mBase->mCloseSound; const ESM::RefId lockedSound = ESM::RefId::stringRefId("LockedDoor"); // 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::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()) { 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); animation->addSpellCastGlow( effect->getColor(), 1); // 1 second glow to match the time taken for a door opening or closing } } 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); if (!keyPtr.isEmpty()) { hasKey = true; keyName = keyPtr.getClass().getName(keyPtr); } } if (isLocked && hasKey) { 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) { 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) { // Trap activation 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()) { // player activated teleport door with telekinesis return std::make_unique(); } else { std::unique_ptr action = std::make_unique( ptr.getCellRef().getDestCell(), ptr.getCellRef().getDoorDest(), true); action->setSound(openSound); return action; } } else { // animated door 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]; if (doorState == MWWorld::DoorState::Opening) opening = false; if (doorState == MWWorld::DoorState::Idle && doorRot != 0) opening = false; if (opening) { 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); 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); action->setSoundOffset(std::max(offset, 0.0f)); action->setSound(closeSound); } return action; } } else { // locked, and we can't open. std::unique_ptr action = std::make_unique(std::string_view{}, ptr); action->setSound(lockedSound); return action; } } bool Door::canLock(const MWWorld::ConstPtr& ptr) const { return true; } bool Door::allowTelekinesis(const MWWorld::ConstPtr& ptr) const { if (ptr.getCellRef().getTeleport() && !ptr.getCellRef().isLocked() && ptr.getCellRef().getTrap().empty()) return false; else return true; } ESM::RefId Door::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } MWGui::ToolTipInfo Door::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; std::string_view name = getName(ptr); info.caption = MyGUI::TextIterator::toTagsString(MyGUI::UString(name)); std::string text; if (ptr.getCellRef().getTeleport()) { text += "\n#{sTo}"; text += "\n" + getDestination(*ref); } int lockLevel = ptr.getCellRef().getLockLevel(); 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()) { info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.text = std::move(text); return info; } std::string Door::getDestination(const MWWorld::LiveCellRef& door) { std::string_view dest = MWBase::Environment::get().getWorld()->getCellName( &MWBase::Environment::get().getWorldModel()->getCell(door.mRef.getDestCell())); return "#{sCell=" + std::string{ dest } + "}"; } MWWorld::Ptr Door::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } void Door::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { ptr.getRefData().setCustomData(std::make_unique()); } } MWWorld::DoorState Door::getDoorState(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData()) return MWWorld::DoorState::Idle; const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); return customData.mDoorState; } 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"); ensureCustomData(ptr); DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); customData.mDoorState = state; } void Door::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; ensureCustomData(ptr); DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); const ESM::DoorState& doorState = state.asDoorState(); customData.mDoorState = MWWorld::DoorState(doorState.mDoorState); } void Door::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } const DoorCustomData& customData = ptr.getRefData().getCustomData()->asDoorCustomData(); ESM::DoorState& doorState = state.asDoorState(); doorState.mDoorState = int(customData.mDoorState); } } openmw-openmw-0.49.0/apps/openmw/mwclass/door.hpp000066400000000000000000000054211503074453300220170ustar00rootroot00000000000000#ifndef GAME_MWCLASS_DOOR_H #define GAME_MWCLASS_DOOR_H #include #include "../mwworld/registeredclass.hpp" namespace MWClass { class Door : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Door(); void ensureCustomData(const MWWorld::Ptr& ptr) const; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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 void insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; void insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const override; bool isDoor() const override; bool useAnim() const override; std::string_view getName(const MWWorld::ConstPtr& ptr) const override; ///< \return name or ID; can return an empty string. 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. static std::string getDestination(const MWWorld::LiveCellRef& door); ///< @return destination cell name or token bool canLock(const MWWorld::ConstPtr& ptr) const override; bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const override; ///< Return whether this class of object can be activated with telekinesis ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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; 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. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/esm4base.cpp000066400000000000000000000024301503074453300225470ustar00rootroot00000000000000#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, VFS::Path::toNormalized(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; } } openmw-openmw-0.49.0/apps/openmw/mwclass/esm4base.hpp000066400000000000000000000133741503074453300225650ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwclass/esm4npc.cpp000066400000000000000000000170761503074453300224310ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/openmw/mwclass/esm4npc.hpp000066400000000000000000000050701503074453300224250ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwclass/ingredient.cpp000066400000000000000000000131771503074453300232060ustar00rootroot00000000000000#include "ingredient.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/actioneat.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.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 { Ingredient::Ingredient() : MWWorld::RegisteredClass(ESM::Ingredient::sRecordId) { } void Ingredient::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Ingredient::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Ingredient::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Ingredient::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Ingredient::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Ingredient::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } std::unique_ptr Ingredient::use(const MWWorld::Ptr& ptr, bool force) const { if (ptr.get()->mBase->mData.mEffectID[0] < 0) return std::make_unique(); std::unique_ptr action = std::make_unique(ptr); action->setSound(ESM::RefId::stringRefId("Swallow")); return action; } const ESM::RefId& Ingredient::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static auto sound = ESM::RefId::stringRefId("Item Ingredient Up"); return sound; } const ESM::RefId& Ingredient::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static auto sound = ESM::RefId::stringRefId("Item Ingredient Down"); return sound; } const std::string& Ingredient::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Ingredient::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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 += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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(); float alchemySkill = player.getClass().getSkill(player, ESM::Skill::Alchemy); static const float fWortChanceValue = MWBase::Environment::get() .getESMStore() ->get() .find("fWortChanceValue") ->mValue.getFloat(); MWGui::Widgets::SpellEffectList list; 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 = 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 = std::move(list); info.text = std::move(text); info.isIngredient = true; return info; } MWWorld::Ptr Ingredient::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Ingredient::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Ingredients) != 0; } float Ingredient::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/ingredient.hpp000066400000000000000000000044261503074453300232100ustar00rootroot00000000000000#ifndef GAME_MWCLASS_INGREDIENT_H #define GAME_MWCLASS_INGREDIENT_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Ingredient : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Ingredient(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::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. std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/itemlevlist.cpp000066400000000000000000000006541503074453300234130ustar00rootroot00000000000000#include "itemlevlist.hpp" #include namespace MWClass { ItemLevList::ItemLevList() : MWWorld::RegisteredClass(ESM::ItemLevList::sRecordId) { } std::string_view ItemLevList::getName(const MWWorld::ConstPtr& ptr) const { return {}; } bool ItemLevList::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } } openmw-openmw-0.49.0/apps/openmw/mwclass/itemlevlist.hpp000066400000000000000000000011631503074453300234140ustar00rootroot00000000000000#ifndef GAME_MWCLASS_ITEMLEVLIST_H #define GAME_MWCLASS_ITEMLEVLIST_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class ItemLevList : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; ItemLevList(); public: 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) }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/light.cpp000066400000000000000000000203301503074453300221520ustar00rootroot00000000000000#include "light.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/failedaction.hpp" #include "../mwworld/inventorystore.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 { 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.mFlags & ESM::Light::OffDefault)); } void Light::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { MWWorld::LiveCellRef* ref = ptr.get(); assert(ref->mBase != nullptr); insertObjectPhysics(ptr, model, rotation, physics); 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); } 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, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } bool Light::useAnim() const { return true; } std::string_view Light::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Light::getName(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); if (ref->mBase->mModel.empty()) return {}; return getNameOrId(ptr); } bool Light::isItem(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mData.mFlags & ESM::Light::Carry; } 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::make_unique(); MWWorld::LiveCellRef* ref = ptr.get(); if (!(ref->mBase->mData.mFlags & ESM::Light::Carry)) return std::make_unique(); return defaultItemActivate(ptr, actor); } ESM::RefId Light::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Light::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { 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)); return std::make_pair(slots_, false); } int Light::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Light::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Misc Up"); return sound; } const ESM::RefId& Light::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Misc Down"); return sound; } const std::string& Light::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } bool Light::hasToolTip(const MWWorld::ConstPtr& ptr) const { return showsInInventory(ptr); } MWGui::ToolTipInfo Light::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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::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()) { info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.text = std::move(text); return info; } bool Light::showsInInventory(const MWWorld::ConstPtr& ptr) const { const ESM::Light* light = ptr.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) return false; return Class::showsInInventory(ptr); } std::unique_ptr Light::use(const MWWorld::Ptr& ptr, bool force) const { 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 { ptr.getCellRef().setChargeFloat(duration); } float Light::getRemainingUsageTime(const MWWorld::ConstPtr& ptr) const { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Light::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Lights) != 0; } float Light::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } std::pair Light::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { const MWWorld::LiveCellRef* ref = ptr.get(); if (!(ref->mBase->mData.mFlags & ESM::Light::Carry)) return { 0, {} }; return { 1, {} }; } ESM::RefId Light::getSound(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mSound; } } openmw-openmw-0.49.0/apps/openmw/mwclass/light.hpp000066400000000000000000000071211503074453300221620ustar00rootroot00000000000000#ifndef GAME_MWCLASS_LIGHT_H #define GAME_MWCLASS_LIGHT_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Light : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Light(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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 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; 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) 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 showsInInventory(const MWWorld::ConstPtr& ptr) const override; bool isItem(const MWWorld::ConstPtr&) const override; std::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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. const ESM::RefId& 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 const 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 void setRemainingUsageTime(const MWWorld::Ptr& ptr, float duration) const override; ///< Sets the remaining duration of the object. float getRemainingUsageTime(const MWWorld::ConstPtr& ptr) const override; ///< Returns the remaining duration of the object. std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) 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 openmw-openmw-0.49.0/apps/openmw/mwclass/light4.cpp000066400000000000000000000013741503074453300222450ustar00rootroot00000000000000#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)); } } openmw-openmw-0.49.0/apps/openmw/mwclass/light4.hpp000066400000000000000000000011101503074453300222360ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwclass/lockpick.cpp000066400000000000000000000125601503074453300226500ustar00rootroot00000000000000#include "lockpick.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.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 { Lockpick::Lockpick() : MWWorld::RegisteredClass(ESM::Lockpick::sRecordId) { } void Lockpick::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Lockpick::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Lockpick::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Lockpick::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Lockpick::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Lockpick::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { std::vector slots_; slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair(slots_, false); } int Lockpick::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Lockpick::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static ESM::RefId sound = ESM::RefId::stringRefId("Item Lockpick Up"); return sound; } const ESM::RefId& Lockpick::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static ESM::RefId sound = ESM::RefId::stringRefId("Item Lockpick Down"); return sound; } const std::string& Lockpick::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Lockpick::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } std::unique_ptr Lockpick::use(const MWWorld::Ptr& ptr, bool force) const { 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 { 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 { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return { 0, "#{sCantEquipWeapWarning}" }; return { 1, {} }; } bool Lockpick::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Picks) != 0; } int Lockpick::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } float Lockpick::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/lockpick.hpp000066400000000000000000000060101503074453300226460ustar00rootroot00000000000000#ifndef GAME_MWCLASS_LOCKPICK_H #define GAME_MWCLASS_LOCKPICK_H #include "../mwworld/registeredclass.hpp" namespace ESM { class RefId; } namespace MWClass { class Lockpick : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Lockpick(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::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 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. const ESM::RefId& 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 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::unique_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; bool canSell(const MWWorld::ConstPtr& item, int npcServices) 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 bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override { return true; } ///< \return Item health data available? (default implementation: false) }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/misc.cpp000066400000000000000000000217331503074453300220060ustar00rootroot00000000000000#include "misc.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionsoulgem.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/nullaction.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 { Miscellaneous::Miscellaneous() : MWWorld::RegisteredClass(ESM::Miscellaneous::sRecordId) { } bool Miscellaneous::isGold(const MWWorld::ConstPtr& ptr) const { 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::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Miscellaneous::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Miscellaneous::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Miscellaneous::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Miscellaneous::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Miscellaneous::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); int value = ref->mBase->mData.mValue; if (isGold(ptr) && ptr.getCellRef().getCount() != 1) value = 1; if (!ptr.getCellRef().getSoul().empty()) { const ESM::Creature* creature = MWBase::Environment::get().getESMStore()->get().search(ref->mRef.getSoul()); if (creature) { int soul = creature->mData.mSoul; if (Settings::game().mRebalanceSoulGemValues) { // 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 (ptr.getCellRef().getRefId() == "Misc_SoulGem_Azura") value += soulValue; else value = soulValue; } else value *= soul; } } return value; } 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 soundGold; return soundMisc; } 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 soundGold; return soundMisc; } const std::string& Miscellaneous::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Miscellaneous::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; bool gold = isGold(ptr); if (gold) count *= getValue(ptr); std::string countString; if (!gold) countString = MWGui::ToolTips::getCountString(count); else // gold displays its count also if it's 1. countString = " (" + std::to_string(count) + ")"; 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.mFlags & ESM::Miscellaneous::Key)) text += MWGui::ToolTips::getValueString(getValue(ptr), "#{sValue}"); 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 = std::move(text); return info; } static MWWorld::Ptr createGold(MWWorld::CellStore& cell, int goldAmount) { 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().setCount(count); } newPtr.getCellRef().unsetRefNum(); newPtr.getRefData().setLuaScripts(nullptr); MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; } MWWorld::Ptr Miscellaneous::moveToCell(const MWWorld::Ptr& ptr, MWWorld::CellStore& cell) const { 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 { 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 { const MWWorld::LiveCellRef* ref = item.get(); return !(ref->mBase->mData.mFlags & ESM::Miscellaneous::Key) && (npcServices & ESM::NPC::Misc) && !isGold(item); } float Miscellaneous::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } 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 { return ptr.getCellRef().getRefId().startsWith("misc_soulgem"); } } openmw-openmw-0.49.0/apps/openmw/mwclass/misc.hpp000066400000000000000000000051131503074453300220050ustar00rootroot00000000000000#ifndef GAME_MWCLASS_MISC_H #define GAME_MWCLASS_MISC_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Miscellaneous : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Miscellaneous(); 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 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. bool isItem(const MWWorld::ConstPtr&) const override { return 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. 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. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; 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; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; bool isKey(const MWWorld::ConstPtr& ptr) const override; bool isGold(const MWWorld::ConstPtr& ptr) const override; bool isSoulGem(const MWWorld::ConstPtr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/nameorid.hpp000066400000000000000000000011431503074453300226470ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwclass/npc.cpp000066400000000000000000001771211503074453300216360ustar00rootroot00000000000000#include "npc.hpp" #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/luamanager.hpp" #include "../mwbase/mechanicsmanager.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/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/weapontype.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/localscripts.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/worldmodel.hpp" #include "../mwrender/npcanimation.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.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) { double int_part; modf(d / 2.0, &int_part); return 2.0 * int_part == d; } int round_ieee_754(double d) { double i = floor(d); d -= i; if (d < 0.5) return static_cast(i); if (d > 0.5) return static_cast(i) + 1; if (is_even(i)) return static_cast(i); return static_cast(i) + 1; } void autoCalculateAttributes(const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) { // race bonus 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 (const ESM::Attribute& attribute : attributes) creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); // class bonus const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); for (int attribute : class_->mData.mAttribute) { if (attribute >= 0 && attribute < ESM::Attribute::Length) { auto id = ESM::Attribute::indexToRefId(attribute); creatureStats.setAttribute(id, creatureStats.getAttribute(id).getBase() + 10); } } // skill bonus for (const ESM::Attribute& attribute : attributes) { float modifierSum = 0; int attributeIndex = ESM::Attribute::refIdToIndex(attribute.mId); for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { if (skill.mData.mAttribute != attributeIndex) continue; // is this a minor or major skill? float add = 0.2f; int index = ESM::Skill::refIdToIndex(skill.mId); for (const auto& skills : class_->mData.mSkills) { if (skills[0] == index) add = 0.5; if (skills[1] == index) add = 1.0; } modifierSum += add; } creatureStats.setAttribute(attribute.mId, std::min( round_ieee_754(creatureStats.getAttribute(attribute.mId).getBase() + (level - 1) * modifierSum), 100)); } // initial health float strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); float endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); int multiplier = 3; if (class_->mData.mSpecialization == ESM::Class::Combat) multiplier += 2; else if (class_->mData.mSpecialization == ESM::Class::Stealth) multiplier += 1; 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)); } /** * @brief autoCalculateSkills * * Skills are calculated with following formulae ( http://www.uesp.net/wiki/Morrowind:NPCs#Skills ): * * Skills: (Level - 1) × (Majority Multiplier + Specialization Multiplier) * * The Majority Multiplier is 1.0 for a Major or Minor Skill, or 0.1 for a Miscellaneous Skill. * * The Specialization Multiplier is 0.5 for a Skill in the same Specialization as the class, * zero for other Skills. * * and by adding class, race, specialization bonus. */ void autoCalculateSkills( const ESM::NPC* npc, MWMechanics::NpcStats& npcStats, const MWWorld::Ptr& ptr, bool spellsInitialised) { const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); unsigned int level = npcStats.getLevel(); 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; for (const auto& skills : class_->mData.mSkills) { ESM::RefId id = ESM::Skill::indexToRefId(skills[i]); if (!id.empty()) { npcStats.getSkill(id).setBase(npcStats.getSkill(id).getBase() + bonus); } } } for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { float majorMultiplier = 0.1f; float specMultiplier = 0.0f; int raceBonus = 0; int specBonus = 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()) raceBonus = bonusIt->mBonus; for (const auto& skills : class_->mData.mSkills) { // is this a minor or major skill? if (std::find(skills.begin(), skills.end(), index) != skills.end()) { majorMultiplier = 1.0f; break; } } // is this skill in the same Specialization as the class? if (skill.mData.mSpecialization == class_->mData.mSpecialization) { specMultiplier = 0.5f; specBonus = 5; } 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 } if (!spellsInitialised) { std::vector spells = MWMechanics::autoCalcNpcSpells(npcStats.getSkills(), npcStats.getAttributes(), race); npcStats.getSpells().addAllToInstance(spells); } } } namespace MWClass { Npc::Npc() : MWWorld::RegisteredClass(ESM::NPC::sRecordId) { } class NpcCustomData : public MWWorld::TypedCustomData { public: MWMechanics::NpcStats mNpcStats; MWMechanics::Movement mMovement; MWWorld::InventoryStore mInventoryStore; NpcCustomData& asNpcCustomData() override { return *this; } const NpcCustomData& asNpcCustomData() const override { return *this; } }; const Npc::GMST& Npc::getGmst() { 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"); gmst.fEncumberedMoveEffect = store.find("fEncumberedMoveEffect"); gmst.fSneakSpeedMultiplier = store.find("fSneakSpeedMultiplier"); gmst.fAthleticsRunBonus = store.find("fAthleticsRunBonus"); gmst.fBaseRunMultiplier = store.find("fBaseRunMultiplier"); gmst.fMinFlySpeed = store.find("fMinFlySpeed"); gmst.fMaxFlySpeed = store.find("fMaxFlySpeed"); gmst.fSwimRunBase = store.find("fSwimRunBase"); gmst.fSwimRunAthleticsMult = store.find("fSwimRunAthleticsMult"); gmst.fJumpEncumbranceBase = store.find("fJumpEncumbranceBase"); gmst.fJumpEncumbranceMultiplier = store.find("fJumpEncumbranceMultiplier"); gmst.fJumpAcrobaticsBase = store.find("fJumpAcrobaticsBase"); gmst.fJumpAcroMultiplier = store.find("fJumpAcroMultiplier"); gmst.fJumpRunMultiplier = store.find("fJumpRunMultiplier"); gmst.fWereWolfRunMult = store.find("fWereWolfRunMult"); gmst.fKnockDownMult = store.find("fKnockDownMult"); gmst.iKnockDownOddsMult = store.find("iKnockDownOddsMult"); gmst.iKnockDownOddsBase = store.find("iKnockDownOddsBase"); gmst.fCombatArmorMinMult = store.find("fCombatArmorMinMult"); return gmst; }(); return staticGmst; } void Npc::ensureCustomData(const MWWorld::Ptr& ptr) const { if (!ptr.getRefData().getCustomData()) { 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(); bool spellsInitialised = data->mNpcStats.getSpells().setSpells(ref->mBase->mId); // creature stats int gold = 0; if (ref->mBase->mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { gold = ref->mBase->mNpdt.mGold; 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]); 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.setLevel(ref->mBase->mNpdt.mLevel); data->mNpcStats.setBaseDisposition(ref->mBase->mNpdt.mDisposition); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); } else { gold = ref->mBase->mNpdt.mGold; 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); data->mNpcStats.setReputation(ref->mBase->mNpdt.mReputation); autoCalculateAttributes(ref->mBase, data->mNpcStats); autoCalculateSkills(ref->mBase, data->mNpcStats, ptr, spellsInitialised); recalculate = true; } // Persistent actors with 0 health do not play death animation if (data->mNpcStats.isDead()) data->mNpcStats.setDeathAnimationFinished(isPersistent(ptr)); // race powers 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() .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.getAiSequence().fill(ref->mBase->mAiPackage); 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); data->mNpcStats.setGoldPool(gold); // store resetter.mPtr = {}; if (recalculate) data->mNpcStats.recalculateMagicka(); // inventory // setting ownership is used to make the NPC auto-equip his initial equipment only, and not bartered items 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 { renderingInterface.getObjects().insertNPC(ptr); } bool Npc::isPersistent(const MWWorld::ConstPtr& actor) const { const MWWorld::LiveCellRef* ref = actor.get(); 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()); } VFS::Path::Normalized Npc::getCorrectedModel(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); 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(); return Settings::models().mBaseanim.get(); } void Npc::getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector& models) const { 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(npc->mBase->mModel); if (!npc->mBase->mHead.empty()) { const ESM::BodyPart* head = esmStore->get().search(npc->mBase->mHead); if (head) models.push_back(head->mModel); } if (!npc->mBase->mHair.empty()) { const ESM::BodyPart* hair = esmStore->get().search(npc->mBase->mHair); if (hair) models.push_back(hair->mModel); } bool female = (npc->mBase->mFlags & ESM::NPC::Female); const MWWorld::CustomData* customData = ptr.getRefData().getCustomData(); if (customData) { const MWWorld::InventoryStore& invStore = customData->asNpcCustomData().mInventoryStore; for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ConstContainerStoreIterator equipped = invStore.getSlot(slot); if (equipped != invStore.end()) { 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 (const ESM::Race* race = esmStore->get().search(npc->mBase->mRace)) { const std::vector& parts = MWRender::NpcAnimation::getBodyParts(race->mId, female, false, false); for (const ESM::BodyPart* part : parts) { if (part && !part->mModel.empty()) models.push_back(part->mModel); } } } std::string_view Npc::getName(const MWWorld::ConstPtr& ptr) const { if (ptr.getRefData().getCustomData() && ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats.isWerewolf()) { const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); return store.find("sWerewolfPopup")->mValue.getString(); } return getNameOrId(ptr); } MWMechanics::CreatureStats& Npc::getCreatureStats(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } MWMechanics::NpcStats& Npc::getNpcStats(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mNpcStats; } bool Npc::evaluateHit(const MWWorld::Ptr& ptr, MWWorld::Ptr& victim, osg::Vec3f& hitPosition) const { victim = MWWorld::Ptr(); hitPosition = osg::Vec3f(); // Get the weapon used (if hand-to-hand, weapon = inv.end()) 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; MWBase::World* world = MWBase::Environment::get().getWorld(); const float dist = MWMechanics::getMeleeWeaponReach(ptr, weapon); const std::pair result = MWMechanics::getHitContact(ptr, dist); if (result.first.isEmpty()) // Didn't hit anything return true; // 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; ESM::RefId weapskill = ESM::Skill::HandToHand; if (!weapon.isEmpty()) weapskill = weapon.getClass().getEquipmentSkill(weapon); float hitchance = MWMechanics::getHitChance(ptr, victim, getSkill(ptr, weapskill)); 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 (!MWMechanics::isInMeleeReach(ptr, victim, MWMechanics::getMeleeWeaponReach(ptr, weapon))) return; if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->setEnemy(victim); float damage = 0.0f; if (!success) { 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; if (!weapon.isEmpty()) { 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); } MWMechanics::adjustWeaponDamage(damage, weapon, ptr); MWMechanics::reduceWeaponCondition(damage, true, weapon, ptr); MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage); MWMechanics::applyWerewolfDamageMult(victim, weapon, damage); healthdmg = true; } else { MWMechanics::getHandToHandDamage(ptr, victim, damage, healthdmg, attackStrength); } MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Store& store = world->getStore().get(); if (ptr == MWMechanics::getPlayer()) { 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) { damage *= store.find("fCombatCriticalStrikeMult")->mValue.getFloat(); MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); if (healthdmg) { MWBase::Environment::get().getSoundManager()->playSound3D( victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); } } } if (othercls.getCreatureStats(victim).getKnockedDown()) damage *= store.find("fCombatKODamageMult")->mValue.getFloat(); // Apply "On hit" enchanted weapons MWMechanics::applyOnStrikeEnchantment(ptr, victim, weapon, hitPosition); MWMechanics::applyElementalShields(ptr, victim); if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; if (victim == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState()) damage = 0; MWMechanics::diseaseContact(victim, ptr); 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 MWMechanics::DamageSourceType sourceType) const { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); MWMechanics::CreatureStats& stats = getCreatureStats(ptr); bool wasDead = stats.isDead(); bool setOnPcHitMe = true; // NOTE: 'object' and/or 'attacker' may be empty. if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker)) { stats.setAttacked(true); 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()) { MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker); // First handle the attacked actor if ((stats.getHitAttemptActorId() == -1) && (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.setHitAttemptActorId(stats.getActorId()); } if (!object.isEmpty()) stats.setLastHitAttemptObject(object.getCellRef().getRefId()); if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const ESM::RefId& script = getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ if (!script.empty()) ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } if (!successful) { // Missed if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer()) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f); return; } if (!object.isEmpty()) stats.setLastHitObject(object.getCellRef().getRefId()); if (damage < 0.001f) damage = 0; bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) damage = 0; if (damage > 0.0f && !attacker.isEmpty()) { // '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().getESMStore(); const GMST& gmst = getGmst(); int chance = store.get().find("iVoiceHitOdds")->mValue.getInteger(); 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 knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f + gmst.iKnockDownOddsBase->mValue.getInteger(); if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng)) stats.setKnockedDown(true); else stats.setHitRecovery(true); // Is this supposed to always occur? if (damage > 0 && ishealth) { // Hit percentages: // cuirass = 30% // shield, helmet, greaves, boots, pauldrons = 10% each // guantlets = 5% each static const int hitslots[20] = { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet }; int hitslot = hitslots[Misc::Rng::rollDice(20, prng)]; float unmitigatedDamage = damage; float x = damage / (damage + getArmorRating(ptr)); damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x); int damageDiff = static_cast(unmitigatedDamage - damage); damage = std::max(1.f, damage); damageDiff = std::max(1, damageDiff); MWWorld::InventoryStore& inv = getInventoryStore(ptr); MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot); MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr()); bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; // If there's no item in the carried left slot or if it is not a shield redistribute the hit. if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft) { if (Misc::Rng::rollDice(2, prng) == 0) hitslot = MWWorld::InventoryStore::Slot_Cuirass; else hitslot = MWWorld::InventoryStore::Slot_LeftPauldron; armorslot = inv.getSlot(hitslot); if (armorslot != inv.end()) { armor = *armorslot; hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId; } } if (hasArmor) { // Unarmed creature attacks don't affect armor condition unless it was // explicitly requested. if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc() || Settings::game().mUnarmedCreatureAttacksDamageArmor) { int armorhealth = armor.getClass().getItemHealth(armor); armorhealth -= std::min(damageDiff, armorhealth); armor.getCellRef().setCharge(armorhealth); // Armor broken? unequip it if (armorhealth == 0) armor = *inv.unequipItem(armor); } ESM::RefId skill = armor.getClass().getEquipmentSkill(armor); if (ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent); if (skill == ESM::Skill::LightArmor) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f); else if (skill == ESM::Skill::MediumArmor) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f); else if (skill == ESM::Skill::HeavyArmor) sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f); } else if (ptr == MWMechanics::getPlayer()) skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent); } } if (ishealth) { if (!attacker.isEmpty() && !godmode) damage = scaleDamage(damage, attacker, ptr); if (damage > 0.0f) { sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f); if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(); if (!attacker.isEmpty()) MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); health.setCurrent(health.getCurrent() - damage); stats.setHealth(health); } else { MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); fatigue.setCurrent(fatigue.getCurrent() - damage, true); stats.setFatigue(fatigue); } if (!wasDead && getCreatureStats(ptr).isDead()) { // NPC was killed if (!attacker.isEmpty() && attacker.getClass().isNpc() && attacker.getClass().getNpcStats(attacker).isWerewolf()) { attacker.getClass().getNpcStats(attacker).addWerewolfKill(); } MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, attacker); } } 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::make_unique(actor); // Werewolfs can't activate NPCs if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::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()) { // 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::make_unique(ptr); } else { 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); } if (inCombatWithActor) return std::make_unique("#{sActorInCombat}"); return std::make_unique(); } MWWorld::ContainerStore& Npc::getContainerStore(const MWWorld::Ptr& ptr) const { return getInventoryStore(ptr); } MWWorld::InventoryStore& Npc::getInventoryStore(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mInventoryStore; } ESM::RefId Npc::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } float Npc::getMaxSpeed(const MWWorld::Ptr& ptr) const { // 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::NpcStats& stats = getNpcStats(ptr); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const MWBase::World* world = MWBase::Environment::get().getWorld(); const GMST& gmst = getGmst(); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const float normalizedEncumbrance = getNormalizedEncumbrance(ptr); bool swimming = world->isSwimming(ptr); bool sneaking = MWBase::Environment::get().getMechanicsManager()->isSneaking(ptr); bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run); bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); running = running && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); float moveSpeed; if (getEncumbrance(ptr) > getCapacity(ptr)) moveSpeed = 0.0f; else if (mageffects.getOrDefault(ESM::MagicEffect::Levitate).getMagnitude() > 0 && world->isLevitationEnabled()) { 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; } else if (swimming) moveSpeed = getSwimSpeed(ptr); else if (running && !sneaking) moveSpeed = getRunSpeed(ptr); else moveSpeed = getWalkSpeed(ptr); if (stats.isWerewolf() && running && stats.getDrawState() == MWMechanics::DrawState::Nothing) moveSpeed *= gmst.fWereWolfRunMult->mValue.getFloat(); return moveSpeed; } float Npc::getJump(const MWWorld::Ptr& ptr) const { if (getEncumbrance(ptr) > getCapacity(ptr)) return 0.f; const MWMechanics::NpcStats& stats = getNpcStats(ptr); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; const GMST& gmst = getGmst(); 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) { b = a - 50.0f; a = 50.0f; } 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.getOrDefault(ESM::MagicEffect::Jump).getMagnitude() * 64; x *= encumbranceTerm; if (stats.getStance(MWMechanics::CreatureStats::Stance_Run)) x *= gmst.fJumpRunMultiplier->mValue.getFloat(); x *= stats.getFatigueTerm(); x -= -Constants::GravityConst * Constants::UnitsPerMeter; x /= 3.0f; return x; } MWMechanics::Movement& Npc::getMovementSettings(const MWWorld::Ptr& ptr) const { ensureCustomData(ptr); return ptr.getRefData().getCustomData()->asNpcCustomData().mMovement; } bool Npc::isEssential(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return (ref->mBase->mFlags & ESM::NPC::Essential) != 0; } bool Npc::hasToolTip(const MWWorld::ConstPtr& ptr) const { if (!ptr.getRefData().getCustomData() || MWBase::Environment::get().getWindowManager()->isGuiMode()) return true; const NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); if (customData.mNpcStats.isDead() && customData.mNpcStats.isDeathAnimationFinished()) return true; const MWMechanics::AiSequence& aiSeq = customData.mNpcStats.getAiSequence(); if (!aiSeq.isInCombat() || aiSeq.isFleeing()) return true; if (Settings::game().mAlwaysAllowStealingFromKnockedOutActors && customData.mNpcStats.getKnockedDown()) return true; return false; } MWGui::ToolTipInfo Npc::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); bool fullHelp = MWBase::Environment::get().getWindowManager()->getFullHelp(); MWGui::ToolTipInfo info; 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.extra = MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); return info; } float Npc::getCapacity(const MWWorld::Ptr& ptr) const { 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 { // 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); } void Npc::skillUsageSucceeded(const MWWorld::Ptr& ptr, ESM::RefId skill, int usageType, float extraFactor) const { MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor); } float Npc::getArmorRating(const MWWorld::Ptr& ptr) const { const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); 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++) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(i); if (it == invStore.end() || it->getType() != ESM::Armor::sRecordId) { // unarmored ratings[i] = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); } else { ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr); // Take in account armor condition const bool hasHealth = it->getClass().hasItemHealth(*it); if (hasHealth) { ratings[i] *= it->getClass().getItemNormalizedHealth(*it); } } } 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; } 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 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. if (ptr == MWMechanics::getPlayer() && ptr.isInCell() && MWBase::Environment::get().getWorld()->isFirstPerson()) { if (ref->mBase->isMale()) scale *= race->mData.mMaleHeight; else scale *= race->mData.mFemaleHeight; return; } if (ref->mBase->isMale()) { scale.x() *= race->mData.mMaleWeight; scale.y() *= race->mData.mMaleWeight; scale.z() *= race->mData.mMaleHeight; } else { scale.x() *= race->mData.mFemaleWeight; scale.y() *= race->mData.mFemaleWeight; scale.z() *= race->mData.mFemaleHeight; } } int Npc::getServices(const MWWorld::ConstPtr& actor) const { 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; } ESM::RefId Npc::getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const { if (name == "left" || name == "right") { 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") ? 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)) { int weaponType = ESM::Weapon::None; MWMechanics::getActiveWeapon(ptr, &weaponType); if (weaponType == ESM::Weapon::None) return ESM::RefId(); } const MWWorld::InventoryStore& inv = Npc::getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator boots = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); 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 ESM::RefId(); } // Morrowind ignores land soundgen for NPCs 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 ESM::RefId(); if (name == "roar") return ESM::RefId(); if (name == "scream") return ESM::RefId(); throw std::runtime_error("Unexpected soundgen type: " + std::string(name)); } MWWorld::Ptr Npc::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { 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, ESM::RefId id) const { return getNpcStats(ptr).getSkill(id).getModified(); } int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const { return ptr.get()->mBase->mBloodType; } void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const { if (!state.mHasCustomState) return; const ESM::NpcState& npcState = state.asNpcState(); if (!ptr.getRefData().getCustomData()) { if (npcState.mCreatureStats.mMissingACDT) ensureCustomData(ptr); else { // Create a CustomData, but don't fill it from ESM records (not needed) auto data = std::make_unique(); MWBase::Environment::get().getWorldModel()->registerPtr(ptr); data->mInventoryStore.setPtr(ptr); ptr.getRefData().setCustomData(std::move(data)); } } NpcCustomData& customData = ptr.getRefData().getCustomData()->asNpcCustomData(); customData.mInventoryStore.readState(npcState.mInventory); customData.mNpcStats.readState(npcState.mNpcStats); bool spellsInitialised = customData.mNpcStats.getSpells().setSpells(ptr.get()->mBase->mId); if (spellsInitialised) customData.mNpcStats.getSpells().clear(); customData.mNpcStats.readState(npcState.mCreatureStats); } void Npc::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const { if (!ptr.getRefData().getCustomData()) { state.mHasCustomState = false; return; } 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; } ESM::NpcState& npcState = state.asNpcState(); 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(); return ref->mBase->mNpdt.mGold; } bool Npc::isClass(const MWWorld::ConstPtr& ptr, std::string_view className) const { return ptr.get()->mBase->mClass == className; } bool Npc::canSwim(const MWWorld::ConstPtr& ptr) const { return true; } bool Npc::canWalk(const MWWorld::ConstPtr& ptr) const { return true; } void Npc::respawn(const MWWorld::Ptr& ptr) const { const MWMechanics::CreatureStats& creatureStats = getCreatureStats(ptr); if (ptr.getCellRef().getCount() > 0 && !creatureStats.isDead()) return; if (!creatureStats.isDeathAnimationFinished()) return; 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.getCellRef().getCount() == 0 ? fCorpseClearDelay : std::min(fCorpseRespawnDelay, fCorpseClearDelay); if (ptr.get()->mBase->mFlags & ESM::NPC::Respawn && creatureStats.getTimeOfDeath() + delay <= MWBase::Environment::get().getWorld()->getTimeStamp()) { if (ptr.getCellRef().hasContentFile()) { if (ptr.getCellRef().getCount() == 0) { 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().asVec3()); MWBase::Environment::get().getWorld()->rotateObject( ptr, ptr.getCellRef().getPosition().asRotationVec3(), MWBase::RotationFlag_none); } } } int Npc::getBaseFightRating(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mAiData.mFight; } bool Npc::isBipedal(const MWWorld::ConstPtr& ptr) const { return true; } ESM::RefId Npc::getPrimaryFaction(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mFaction; } int Npc::getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const { const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); if (factionID.empty()) return -1; // Search in the NPC data first if (const MWWorld::CustomData* data = ptr.getRefData().getCustomData()) { int rank = data->asNpcCustomData().mNpcStats.getFactionRank(factionID); if (rank >= 0) return rank; } // Use base NPC record as a fallback const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->getFactionRank(); } void Npc::setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const { MWMechanics::setBaseAISetting(id, setting, value); } void Npc::modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const { MWMechanics::modifyBaseInventory(actorId, itemId, amount); } float Npc::getWalkSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); 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 * stats.getAttribute(ESM::Attribute::Speed).getModified() * (gmst.fMaxWalkSpeed->mValue.getFloat() - gmst.fMinWalkSpeed->mValue.getFloat()); walkSpeed *= 1.0f - gmst.fEncumberedMoveEffect->mValue.getFloat() * normalizedEncumbrance; walkSpeed = std::max(0.0f, walkSpeed); if (sneaking) walkSpeed *= gmst.fSneakSpeedMultiplier->mValue.getFloat(); return walkSpeed; } float Npc::getRunSpeed(const MWWorld::Ptr& ptr) const { const GMST& gmst = getGmst(); return getWalkSpeed(ptr) * (0.01f * getSkill(ptr, ESM::Skill::Athletics) * gmst.fAthleticsRunBonus->mValue.getFloat() + gmst.fBaseRunMultiplier->mValue.getFloat()); } float Npc::getSwimSpeed(const MWWorld::Ptr& ptr) const { const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWMechanics::NpcStats& stats = getNpcStats(ptr); const MWMechanics::MagicEffects& mageffects = stats.getMagicEffects(); const bool swimming = world->isSwimming(ptr); const bool inair = !world->isOnGround(ptr) && !swimming && !world->isFlying(ptr); const bool running = stats.getStance(MWMechanics::CreatureStats::Stance_Run) && (inair || MWBase::Environment::get().getMechanicsManager()->isRunning(ptr)); return getSwimSpeedImpl(ptr, getGmst(), mageffects, running ? getRunSpeed(ptr) : getWalkSpeed(ptr)); } } openmw-openmw-0.49.0/apps/openmw/mwclass/npc.hpp000066400000000000000000000173201503074453300216350ustar00rootroot00000000000000#ifndef GAME_MWCLASS_NPC_H #define GAME_MWCLASS_NPC_H #include "../mwworld/registeredclass.hpp" #include "actor.hpp" namespace ESM { struct GameSetting; } namespace MWClass { class Npc : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Npc(); void ensureCustomData(const MWWorld::Ptr& ptr) const; 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; }; static const GMST& getGmst(); 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_view 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::NpcStats& getNpcStats(const MWWorld::Ptr& ptr) const override; ///< Return NPC stats 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) 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 bool hasInventoryStore(const MWWorld::ConstPtr& 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 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 MWMechanics::DamageSourceType sourceType) const override; 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::unique_ptr activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation 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 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. 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 getArmorRating(const MWWorld::Ptr& ptr) const override; ///< @return combined armor rating of this actor 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. 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; bool isPersistent(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getSoundIdFromSndGen(const MWWorld::Ptr& ptr, std::string_view name) const override; std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; VFS::Path::Normalized getCorrectedModel(const MWWorld::ConstPtr& ptr) 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; 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 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; bool isClass(const MWWorld::ConstPtr& ptr, std::string_view className) 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; void respawn(const MWWorld::Ptr& ptr) const override; int getBaseFightRating(const MWWorld::ConstPtr& ptr) const override; ESM::RefId getPrimaryFaction(const MWWorld::ConstPtr& ptr) const override; int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const override; void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) 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 getRunSpeed(const MWWorld::Ptr& ptr) const override; float getSwimSpeed(const MWWorld::Ptr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/potion.cpp000066400000000000000000000111611503074453300223550ustar00rootroot00000000000000#include "potion.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include #include #include "../mwworld/actionapply.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/ptr.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "../mwmechanics/alchemy.hpp" #include "../mwmechanics/spellutil.hpp" #include "classmodel.hpp" #include "nameorid.hpp" namespace MWClass { Potion::Potion() : MWWorld::RegisteredClass(ESM::Potion::sRecordId) { } void Potion::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Potion::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Potion::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Potion::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Potion::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Potion::getValue(const MWWorld::ConstPtr& ptr) const { return MWMechanics::getPotionValue(*ptr.get()->mBase); } const ESM::RefId& Potion::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Potion Up"); return sound; } const ESM::RefId& Potion::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static const auto sound = ESM::RefId::stringRefId("Item Potion Down"); return sound; } const std::string& Potion::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Potion::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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(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 (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()) { info.extra += MWGui::ToolTips::getCellRefString(ptr.getCellRef()); info.extra += MWGui::ToolTips::getMiscString(ref->mBase->mScript.getRefIdString(), "Script"); } info.text = std::move(text); return info; } std::unique_ptr Potion::use(const MWWorld::Ptr& ptr, bool force) const { MWWorld::LiveCellRef* ref = ptr.get(); auto action = std::make_unique(ptr, ref->mBase->mId); action->setSound(ESM::RefId::stringRefId("Drink")); return action; } MWWorld::Ptr Potion::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } bool Potion::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Potions) != 0; } float Potion::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/potion.hpp000066400000000000000000000043761503074453300223740ustar00rootroot00000000000000#ifndef GAME_MWCLASS_POTION_H #define GAME_MWCLASS_POTION_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Potion : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Potion(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::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. std::unique_ptr use(const MWWorld::Ptr& ptr, bool force = false) const override; ///< Generate action for using via inventory menu const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/probe.cpp000066400000000000000000000123731503074453300221620ustar00rootroot00000000000000#include "probe.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.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 { Probe::Probe() : MWWorld::RegisteredClass(ESM::Probe::sRecordId) { } void Probe::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Probe::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Probe::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Probe::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Probe::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Probe::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { std::vector slots_; slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair(slots_, false); } int Probe::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Probe::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static const ESM::RefId sound = ESM::RefId::stringRefId("Item Probe Up"); return sound; } const ESM::RefId& Probe::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static const ESM::RefId sound = ESM::RefId::stringRefId("Item Probe Down"); return sound; } const std::string& Probe::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Probe::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } std::unique_ptr Probe::use(const MWWorld::Ptr& ptr, bool force) const { 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 { 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 { // Do not allow equip tools from inventory during attack if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc) && MWBase::Environment::get().getWindowManager()->isGuiMode()) return { 0, "#{sCantEquipWeapWarning}" }; return { 1, {} }; } bool Probe::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Probes) != 0; } int Probe::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } float Probe::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/probe.hpp000066400000000000000000000057221503074453300221670ustar00rootroot00000000000000#ifndef GAME_MWCLASS_PROBE_H #define GAME_MWCLASS_PROBE_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Probe : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Probe(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::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 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. const ESM::RefId& 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 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::unique_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; bool canSell(const MWWorld::ConstPtr& item, int npcServices) 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 bool hasItemHealth(const MWWorld::ConstPtr& ptr) const override { return true; } ///< \return Item health data available? (default implementation: false) }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/repair.cpp000066400000000000000000000107561503074453300223400ustar00rootroot00000000000000#include "repair.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.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 { Repair::Repair() : MWWorld::RegisteredClass(ESM::Repair::sRecordId) { } void Repair::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Repair::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Repair::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } std::unique_ptr Repair::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const { return defaultItemActivate(ptr, actor); } ESM::RefId Repair::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } int Repair::getValue(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Repair::getUpSoundId(const MWWorld::ConstPtr& ptr) const { static auto val = ESM::RefId::stringRefId("Item Repair Up"); return val; } const ESM::RefId& Repair::getDownSoundId(const MWWorld::ConstPtr& ptr) const { static auto val = ESM::RefId::stringRefId("Item Repair Down"); return val; } const std::string& Repair::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } bool Repair::hasItemHealth(const MWWorld::ConstPtr& ptr) const { return true; } int Repair::getItemMaxHealth(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mUses; } MWGui::ToolTipInfo Repair::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); MWGui::ToolTipInfo info; 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; int remainingUses = getItemHealth(ptr); text += "\n#{sUses}: " + MWGui::ToolTips::toString(remainingUses); text += "\n#{sQuality}: " + MWGui::ToolTips::toString(ref->mBase->mData.mQuality); text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); 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 = std::move(text); return info; } MWWorld::Ptr Repair::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } std::unique_ptr Repair::use(const MWWorld::Ptr& ptr, bool force) const { return std::make_unique(ptr, force); } bool Repair::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::RepairItem) != 0; } float Repair::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/repair.hpp000066400000000000000000000052701503074453300223400ustar00rootroot00000000000000#ifndef GAME_MWCLASS_REPAIR_H #define GAME_MWCLASS_REPAIR_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Repair : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Repair(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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_view 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::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. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. std::string_view getModel(const MWWorld::ConstPtr& ptr) const override; 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) 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; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/static.cpp000066400000000000000000000037271503074453300223450ustar00rootroot00000000000000#include "static.hpp" #include #include #include #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 { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Static); } } void Static::insertObject(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { insertObjectPhysics(ptr, model, rotation, physics); } void Static::insertObjectPhysics(const MWWorld::Ptr& ptr, const std::string& model, const osg::Quat& rotation, MWPhysics::PhysicsSystem& physics) const { physics.addObject(ptr, VFS::Path::toNormalized(model), rotation, MWPhysics::CollisionType_World); } std::string_view Static::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Static::getName(const MWWorld::ConstPtr& ptr) const { return {}; } bool Static::hasToolTip(const MWWorld::ConstPtr& ptr) const { return false; } MWWorld::Ptr Static::copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } } openmw-openmw-0.49.0/apps/openmw/mwclass/static.hpp000066400000000000000000000026101503074453300223400ustar00rootroot00000000000000#ifndef GAME_MWCLASS_STATIC_H #define GAME_MWCLASS_STATIC_H #include "../mwworld/cellstore.hpp" #include "../mwworld/registeredclass.hpp" #include namespace MWClass { class Static : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; Static(); MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) 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 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_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::string_view getModel(const MWWorld::ConstPtr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwclass/weapon.cpp000066400000000000000000000275471503074453300223550ustar00rootroot00000000000000#include "weapon.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/actionequip.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/ptr.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwgui/tooltips.hpp" #include "../mwrender/objects.hpp" #include "../mwrender/renderinginterface.hpp" #include "classmodel.hpp" #include "nameorid.hpp" namespace MWClass { Weapon::Weapon() : MWWorld::RegisteredClass(ESM::Weapon::sRecordId) { } void Weapon::insertObjectRendering( const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const { if (!model.empty()) { renderingInterface.getObjects().insertModel(ptr, model); } } std::string_view Weapon::getModel(const MWWorld::ConstPtr& ptr) const { return getClassModel(ptr); } std::string_view Weapon::getName(const MWWorld::ConstPtr& ptr) const { return getNameOrId(ptr); } 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 { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mHealth; } ESM::RefId Weapon::getScript(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mScript; } std::pair, bool> Weapon::getEquipmentSlots(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); ESM::WeaponType::Class weapClass = MWMechanics::getWeaponType(ref->mBase->mData.mType)->mWeaponClass; std::vector slots_; bool stack = false; if (weapClass == ESM::WeaponType::Ammo) { slots_.push_back(int(MWWorld::InventoryStore::Slot_Ammunition)); stack = true; } else if (weapClass == ESM::WeaponType::Thrown) { slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); stack = true; } else slots_.push_back(int(MWWorld::InventoryStore::Slot_CarriedRight)); return std::make_pair(slots_, stack); } ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mValue; } const ESM::RefId& Weapon::getUpSoundId(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mSoundIdUp; } const ESM::RefId& Weapon::getDownSoundId(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); int type = ref->mBase->mData.mType; return MWMechanics::getWeaponType(type)->mSoundIdDown; } const std::string& Weapon::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mIcon; } MWGui::ToolTipInfo Weapon::getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const { const MWWorld::LiveCellRef* ref = ptr.get(); const ESM::WeaponType* weaponType = MWMechanics::getWeaponType(ref->mBase->mData.mType); MWGui::ToolTipInfo info; 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().getESMStore(); std::string text; // weapon type & damage if (weaponType->mWeaponClass != ESM::WeaponType::Ammo || Settings::game().mShowProjectileDamage) { text += "\n#{sType} "; 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) oneOrTwoHanded = "sTwoHanded"; else oneOrTwoHanded = "sOneHanded"; } 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)) + " - " + 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])); // Slash 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])) + " - " + 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])) + " - " + MWGui::ToolTips::toString(static_cast(ref->mBase->mData.mChop[1])); } } if (hasItemHealth(ptr)) { int remainingHealth = getItemHealth(ptr); text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/" + MWGui::ToolTips::toString(ref->mBase->mData.mHealth); } 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; text += MWGui::ToolTips::getWeightString(combatDistance / Constants::UnitsPerFoot, "#{sRange}"); text += " #{sFeet}"; } // add attack speed for any weapon excepts arrows and bolts if (weaponType->mWeaponClass != ESM::WeaponType::Ammo && verbose) { text += MWGui::ToolTips::getPercentString(ref->mBase->mData.mSpeed, "#{sAttributeSpeed}"); } text += MWGui::ToolTips::getWeightString(ref->mBase->mData.mWeight, "#{sWeight}"); text += MWGui::ToolTips::getValueString(ref->mBase->mData.mValue, "#{sValue}"); info.enchant = ref->mBase->mEnchant; if (!info.enchant.empty()) info.remainingEnchantCharge = static_cast(ptr.getCellRef().getEnchantmentCharge()); 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 = std::move(text); return info; } ESM::RefId Weapon::getEnchantment(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mEnchant; } 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(); ESM::Weapon newItem = *ref->mBase; 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().getESMStore()->insert(newItem); return record->mId; } std::pair Weapon::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { if (hasItemHealth(ptr) && getItemHealth(ptr) == 0) return { 0, "#{sInventoryMessage1}" }; // Do not allow equip weapons from inventory during attack if (npc.isInCell() && MWBase::Environment::get().getWindowManager()->isGuiMode() && MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(npc)) return { 0, "#{sCantEquipWeapWarning}" }; std::pair, bool> slots_ = getEquipmentSlots(ptr); if (slots_.first.empty()) return { 0, {} }; int type = ptr.get()->mBase->mData.mType; if (MWMechanics::getWeaponType(type)->mFlags & ESM::WeaponType::TwoHanded) { return { 2, {} }; } return { 1, {} }; } std::unique_ptr Weapon::use(const MWWorld::Ptr& ptr, bool force) const { 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 { const MWWorld::LiveCellRef* ref = ptr.get(); return MWWorld::Ptr(cell.insert(ref), &cell); } int Weapon::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mEnchant; } bool Weapon::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return (npcServices & ESM::NPC::Weapon) || ((npcServices & ESM::NPC::MagicItems) && !getEnchantment(item).empty()); } float Weapon::getWeight(const MWWorld::ConstPtr& ptr) const { const MWWorld::LiveCellRef* ref = ptr.get(); return ref->mBase->mData.mWeight; } } openmw-openmw-0.49.0/apps/openmw/mwclass/weapon.hpp000066400000000000000000000073701503074453300223520ustar00rootroot00000000000000#ifndef GAME_MWCLASS_WEAPON_H #define GAME_MWCLASS_WEAPON_H #include "../mwworld/registeredclass.hpp" namespace MWClass { class Weapon : public MWWorld::RegisteredClass { friend MWWorld::RegisteredClass; MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr& ptr, MWWorld::CellStore& cell) const override; public: Weapon(); 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. bool isItem(const MWWorld::ConstPtr&) const override { return 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 hasItemHealth(const MWWorld::ConstPtr& ptr) const override; ///< \return Item health data available? int getItemMaxHealth(const MWWorld::ConstPtr& ptr) const override; ///< Return item max health or throw an exception, if class does not have item health ESM::RefId getScript(const MWWorld::ConstPtr& ptr) const override; ///< Return name of the script attached to ptr 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 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. const ESM::RefId& 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 const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const override; ///< Return name of inventory icon. ESM::RefId getEnchantment(const MWWorld::ConstPtr& ptr) const override; ///< @return the enchantment ID if the object is enchanted, otherwise an empty string 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::unique_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; bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override; float getWeight(const MWWorld::ConstPtr& ptr) const override; int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/000077500000000000000000000000001503074453300210255ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwdialogue/dialoguemanagerimp.cpp000066400000000000000000000645611503074453300253770ustar00rootroot00000000000000#include "dialoguemanagerimp.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/journal.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/extensions.hpp" #include "../mwscript/interpretercontext.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.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) , mOriginalDisposition(0) , mCurrentDisposition(0) , mPermanentDispositionChange(0) { mChoice = -1; mIsInChoice = false; mGoodbye = false; mCompilerContext.setExtensions(&extensions); } void DialogueManager::clear() { mKnownTopics.clear(); mTalkedTo = false; mOriginalDisposition = 0; mCurrentDisposition = 0; mPermanentDispositionChange = 0; } void DialogueManager::addTopic(const ESM::RefId& topic) { mKnownTopics.insert(topic); } std::vector DialogueManager::parseTopicIdsFromText(const std::string& text) { std::vector topicIdList; std::vector hypertext = HyperTextParser::parseHyperText(text); for (std::vector::iterator tok = hypertext.begin(); tok != hypertext.end(); ++tok) { std::string topicId = Misc::StringUtils::lowerCase(tok->mText); if (tok->isExplicitLink()) { // calculation of standard form for all hyperlinks size_t asterisk_count = HyperTextParser::removePseudoAsterisks(topicId); for (; asterisk_count > 0; --asterisk_count) topicId.append("*"); topicId = mTranslationDataStorage.topicStandardForm(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); } } 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(); // Dialogue with dead actor (e.g. through script) should not be allowed. if (actor.getClass().getCreatureStats(actor).isDead()) return false; mLastTopic = ESM::RefId(); // Note that we intentionally don't reset mPermanentDispositionChange mChoice = -1; mIsInChoice = false; mGoodbye = false; mChoices.clear(); mActor = actor; MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); mTalkedTo = creatureStats.hasTalkedToPlayer(); mActorKnownTopics.clear(); // greeting const MWWorld::Store& dialogs = MWBase::Environment::get().getESMStore()->get(); Filter filter(actor, mChoice, mTalkedTo); for (const ESM::Dialogue& dialogue : dialogs) { 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(dialogue, false).second) { creatureStats.talkedToPlayer(); if (!info->mSound.empty()) { // TODO play sound } MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); callback->addResponse({}, Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); executeScript(info->mResultScript, mActor); mLastTopic = dialogue.mId; addTopicsFromText(info->mResponse); return true; } } } return false; } std::optional DialogueManager::compile(const std::string& cmd, const MWWorld::Ptr& actor) { bool success = true; std::optional program; try { mErrorHandler.reset(); mErrorHandler.setContext("[dialogue script]"); std::istringstream input(cmd + "\n"); Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); Compiler::Locals locals; 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); } Compiler::ScriptParser parser(mErrorHandler, mCompilerContext, locals, false); scanner.scan(parser); if (!mErrorHandler.isGood()) success = false; if (success) program = parser.getProgram(); } 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; } if (!success) { Log(Debug::Error) << "Error: compiling failed (dialogue script): \n" << cmd << "\n"; } return program; } void DialogueManager::executeScript(const std::string& script, const MWWorld::Ptr& 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(*program, interpreterContext); } catch (const std::exception& error) { Log(Debug::Error) << std::string("Dialogue error: An exception has been thrown: ") + error.what(); } } } bool DialogueManager::inJournal(const ESM::RefId& topicId, const ESM::RefId& infoId) const { 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) { topicHistory = &it->second; break; } } if (!topicHistory) return false; for (const auto& topic : *topicHistory) { if (topic.mInfoId == infoId) return true; } return false; } void DialogueManager::executeTopic(const ESM::RefId& topic, ResponseCallback* callback) { Filter filter(mActor, mChoice, mTalkedTo); const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); const ESM::Dialogue& dialogue = *dialogues.find(topic); const auto [responseTopic, info] = filter.search(dialogue, true); if (info) { 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.getRefIdString(); modifiedTopic.erase(std::remove(modifiedTopic.begin(), modifiedTopic.end(), ' '), modifiedTopic.end()); const MWWorld::Store& gmsts = MWBase::Environment::get().getESMStore()->get(); title = gmsts.find(modifiedTopic)->mValue.getString(); } else title = dialogue.mStringId; 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. if (responseTopic == &dialogue) MWBase::Environment::get().getJournal()->addTopic(topic, info->mId, mActor); } mLastTopic = topic; executeScript(info->mResultScript, mActor); addTopicsFromText(info->mResponse); } } const ESM::Dialogue* DialogueManager::searchDialogue(const ESM::RefId& id) { return MWBase::Environment::get().getESMStore()->get().search(id); } void DialogueManager::updateGlobals() { MWBase::Environment::get().getWorld()->updateDialogueGlobals(); } void DialogueManager::updateActorKnownTopics() { updateGlobals(); mActorKnownTopics.clear(); const auto& dialogs = MWBase::Environment::get().getESMStore()->get(); Filter filter(mActor, -1, mTalkedTo); for (const auto& dialog : dialogs) { if (dialog.mType == ESM::Dialogue::Topic) { const auto* answer = filter.search(dialog, true).second; const auto& topicId = dialog.mId; if (answer != nullptr) { int topicFlags = 0; if (!inJournal(topicId, answer->mId)) { // Does this dialogue contains some actor-specific answer? if (answer->mActor == mActor.getCellRef().getRefId()) topicFlags |= MWBase::DialogueManager::TopicType::Specific; } else topicFlags |= MWBase::DialogueManager::TopicType::Exhausted; mActorKnownTopics.insert(std::make_pair(dialog.mId, ActorKnownTopicInfo{ topicFlags, answer })); } } } // If response to a topic leads to a new topic, the original topic is not exhausted. for (auto& [dialogId, topicInfo] : mActorKnownTopics) { // If the topic is not marked as exhausted, we don't need to do anything about it. // If the topic will not be shown to the player, the flag actually does not matter. if (!(topicInfo.mFlags & MWBase::DialogueManager::TopicType::Exhausted) || !mKnownTopics.count(dialogId)) continue; for (const auto& topicId : parseTopicIdsFromText(topicInfo.mInfo->mResponse)) { if (mActorKnownTopics.count(topicId) && !mKnownTopics.count(topicId)) { topicInfo.mFlags &= ~MWBase::DialogueManager::TopicType::Exhausted; break; } } } } std::list DialogueManager::getAvailableTopics() { updateActorKnownTopics(); std::list keywordList; const auto& store = MWBase::Environment::get().getESMStore()->get(); for (const auto& [topic, topicInfo] : mActorKnownTopics) { // 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 keywordList.sort(Misc::StringUtils::ciLess); return keywordList; } int DialogueManager::getTopicFlag(const ESM::RefId& topicId) const { auto known = mActorKnownTopics.find(topicId); if (known != mActorKnownTopics.end()) return known->second.mFlags; return 0; } void DialogueManager::keywordSelected(std::string_view keyword, ResponseCallback* callback) { if (!mIsInChoice) { const ESM::Dialogue* dialogue = searchDialogue(ESM::RefId::stringRefId(keyword)); if (dialogue && dialogue->mType == ESM::Dialogue::Topic) { executeTopic(dialogue->mId, callback); } } } bool DialogueManager::isInChoice() const { return mIsInChoice; } void DialogueManager::goodbyeSelected() { // Apply disposition change to NPC's base disposition if we **think** we need to change something if ((mPermanentDispositionChange || mOriginalDisposition != mCurrentDisposition) && mActor.getClass().isNpc()) { updateOriginalDisposition(); MWMechanics::NpcStats& npcStats = mActor.getClass().getNpcStats(mActor); // Clamp permanent disposition change so that final disposition doesn't go below 0 (could happen with // intimidate) npcStats.setBaseDisposition(0); int zero = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor, false); int disposition = std::clamp(mOriginalDisposition + mPermanentDispositionChange, -zero, 100 - zero); npcStats.setBaseDisposition(disposition); } mPermanentDispositionChange = 0; mOriginalDisposition = 0; mCurrentDisposition = 0; } void DialogueManager::questionAnswered(int answer, ResponseCallback* callback) { mChoice = answer; const ESM::Dialogue* dialogue = searchDialogue(mLastTopic); if (dialogue) { Filter filter(mActor, mChoice, mTalkedTo); if (dialogue->mType == ESM::Dialogue::Topic || dialogue->mType == ESM::Dialogue::Greeting) { const auto [responseTopic, info] = filter.search(*dialogue, true); if (info) { 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)); 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 if (responseTopic == dialogue) MWBase::Environment::get().getJournal()->addTopic(mLastTopic, info->mId, mActor); } executeScript(info->mResultScript, mActor); } else { mChoice = -1; mIsInChoice = false; mChoices.clear(); } } } updateActorKnownTopics(); } void DialogueManager::addChoice(std::string_view text, int choice) { mIsInChoice = true; mChoices.emplace_back(text, choice); } const std::vector>& DialogueManager::getChoices() const { return mChoices; } bool DialogueManager::isGoodbye() const { return mGoodbye; } void DialogueManager::goodbye() { mIsInChoice = false; mGoodbye = true; } void DialogueManager::persuade(int type, ResponseCallback* callback) { bool success; int temp, perm; MWBase::Environment::get().getMechanicsManager()->getPersuasionDispositionChange( 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; MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().skillUsageSucceeded( player, ESM::Skill::Speechcraft, success ? ESM::Skill::Speechcraft_Success : ESM::Skill::Speechcraft_Fail); if (success) { int gold = 0; if (type == MWBase::MechanicsManager::PT_Bribe10) gold = 10; else if (type == MWBase::MechanicsManager::PT_Bribe100) gold = 100; else if (type == MWBase::MechanicsManager::PT_Bribe1000) gold = 1000; if (gold) { player.getClass().getContainerStore(player).remove(MWWorld::ContainerStore::sGoldId, gold); mActor.getClass().getContainerStore(mActor).add(MWWorld::ContainerStore::sGoldId, gold); } } std::string text; if (type == MWBase::MechanicsManager::PT_Admire) text = "Admire"; else if (type == MWBase::MechanicsManager::PT_Taunt) text = "Taunt"; else if (type == MWBase::MechanicsManager::PT_Intimidate) text = "Intimidate"; else { text = "Bribe"; } executeTopic(ESM::RefId::stringRefId(text + (success ? " Success" : " Fail")), callback); } void DialogueManager::applyBarterDispositionChange(int 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); const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); const ESM::Dialogue& dialogue = *dialogues.find(ESM::RefId::stringRefId("Service Refusal")); std::vector infos = filter.list(dialogue, false, false, true); if (!infos.empty()) { const ESM::DialInfo* info = infos[0].second; addTopicsFromText(info->mResponse); const MWWorld::Store& gmsts = MWBase::Environment::get().getESMStore()->get(); MWScript::InterpreterContext interpreterContext(&mActor.getRefData().getLocals(), mActor); callback->addResponse(gmsts.find("sServiceRefusal")->mValue.getString(), Interpreter::fixDefinesDialog(info->mResponse, interpreterContext)); executeScript(info->mResultScript, mActor); return true; } return false; } bool DialogueManager::say(const MWWorld::Ptr& actor, const ESM::RefId& topic) { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (sndMgr->sayActive(actor)) { // Actor is already saying something. return false; } if (actor.getClass().isNpc() && MWBase::Environment::get().getWorld()->isSwimming(actor)) { // NPCs don't talk while submerged return false; } if (actor.getClass().getCreatureStats(actor).getKnockedDown()) { // Unconscious actors can not speak return false; } 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).second; if (info != nullptr) { MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); if (Settings::gui().mSubtitles) winMgr->messageBox(info->mResponse); if (!info->mSound.empty()) 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 { return 1; // known topics } void DialogueManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { ESM::DialogueState state; 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); } void DialogueManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_DIAS) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); ESM::DialogueState state; state.load(reader); for (const auto& knownTopic : state.mKnownTopics) if (store.get().search(knownTopic)) mKnownTopics.insert(knownTopic); mChangedFactionReaction = state.mChangedFactionReaction; } } void DialogueManager::modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) { // Make sure the factions exist MWBase::Environment::get().getESMStore()->get().find(faction1); MWBase::Environment::get().getESMStore()->get().find(faction2); int newValue = getFactionReaction(faction1, faction2) + diff; auto& map = mChangedFactionReaction[faction1]; map[faction2] = newValue; } void DialogueManager::setFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int absolute) { // Make sure the factions exist MWBase::Environment::get().getESMStore()->get().find(faction1); MWBase::Environment::get().getESMStore()->get().find(faction2); auto& map = mChangedFactionReaction[faction1]; map[faction2] = absolute; } int DialogueManager::getFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2) const { 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().getESMStore()->get().find(faction1); auto it = faction->mReactions.begin(); for (; it != faction->mReactions.end(); ++it) { if (it->first == faction2) return it->second; } return 0; } 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( mLastTopic, actor.getClass().getName(actor)); } } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/dialoguemanagerimp.hpp000066400000000000000000000110561503074453300253730ustar00rootroot00000000000000#ifndef GAME_MWDIALOG_DIALOGUEMANAGERIMP_H #define GAME_MWDIALOG_DIALOGUEMANAGERIMP_H #include "../mwbase/dialoguemanager.hpp" #include #include #include #include #include #include #include #include #include #include "../mwworld/ptr.hpp" #include "../mwscript/compilercontext.hpp" namespace ESM { struct Dialogue; } namespace MWDialogue { class DialogueManager : public MWBase::DialogueManager { struct ActorKnownTopicInfo { int mFlags; const ESM::DialInfo* mInfo; }; std::set mKnownTopics; // Those are the topics the player knows. // Modified faction reactions. > typedef std::map> ModFactionReactionMap; ModFactionReactionMap mChangedFactionReaction; std::map mActorKnownTopics; Translation::Storage& mTranslationDataStorage; MWScript::CompilerContext mCompilerContext; Compiler::StreamErrorHandler mErrorHandler; MWWorld::Ptr mActor; bool mTalkedTo; int mChoice; ESM::RefId mLastTopic; // last topic ID, lowercase bool mIsInChoice; bool mGoodbye; std::vector> mChoices; int mOriginalDisposition; int mCurrentDisposition; int mPermanentDispositionChange; std::vector parseTopicIdsFromText(const std::string& text); void addTopicsFromText(const std::string& text); void updateActorKnownTopics(); void updateGlobals(); std::optional compile(const std::string& cmd, const MWWorld::Ptr& actor); void executeScript(const std::string& script, const MWWorld::Ptr& actor); void executeTopic(const ESM::RefId& topic, ResponseCallback* callback); const ESM::Dialogue* searchDialogue(const ESM::RefId& id); void updateOriginalDisposition(); public: DialogueManager(const Compiler::Extensions& extensions, Translation::Storage& translationDataStorage); void clear() override; bool isInChoice() const override; bool startDialogue(const MWWorld::Ptr& actor, ResponseCallback* callback) override; std::list getAvailableTopics() override; int getTopicFlag(const ESM::RefId& topicId) const override; bool inJournal(const ESM::RefId& topicId, const ESM::RefId& infoId) const override; void addTopic(const ESM::RefId& topic) override; void addChoice(std::string_view text, int choice) override; const std::vector>& getChoices() const override; bool isGoodbye() const override; void goodbye() override; bool checkServiceRefused(ResponseCallback* callback, ServiceType service = ServiceType::Any) override; bool say(const MWWorld::Ptr& actor, const ESM::RefId& topic) override; // calbacks for the GUI void keywordSelected(std::string_view keyword, ResponseCallback* callback) override; void goodbyeSelected() override; void questionAnswered(int answer, ResponseCallback* callback) override; void persuade(int type, ResponseCallback* callback) override; /// @note Controlled by an option, gets discarded when dialogue ends by default void applyBarterDispositionChange(int delta) override; int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; /// Changes faction1's opinion of faction2 by \a diff. void modFactionReaction(const ESM::RefId& faction1, const ESM::RefId& faction2, int diff) 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/filter.cpp000066400000000000000000000652521503074453300230300ustar00rootroot00000000000000#include "filter.hpp" #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/npcstats.hpp" #include "selectwrapper.hpp" namespace { 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 (info.mActor != mActor.getCellRef().getRefId()) return false; } else if (isCreature) { // Creatures must not have topics aside of those specific to their id return false; } // NPC race if (!info.mRace.empty()) { if (isCreature) return true; MWWorld::LiveCellRef* cellRef = mActor.get(); if (!(info.mRace == cellRef->mBase->mRace)) return false; } // NPC class if (!info.mClass.empty()) { if (isCreature) return true; MWWorld::LiveCellRef* cellRef = mActor.get(); if (!(info.mClass == cellRef->mBase->mClass)) return false; } // NPC faction if (info.mFactionLess) { if (isCreature) return true; if (!mActor.getClass().getPrimaryFaction(mActor).empty()) return false; } else if (!info.mFaction.empty()) { if (isCreature) return true; if (!(mActor.getClass().getPrimaryFaction(mActor) == info.mFaction)) return false; // check rank if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } else if (info.mData.mRank != -1) { if (isCreature) return true; // Rank requirement, but no faction given. Use the actor's faction, if there is one. // check rank if (mActor.getClass().getPrimaryFactionRank(mActor) < info.mData.mRank) return false; } // Gender if (!isCreature) { MWWorld::LiveCellRef* npc = mActor.get(); 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 { const MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::NpcStats& stats = player.getClass().getNpcStats(player); // check player faction and rank if (!info.mPcFaction.empty()) { std::map::const_iterator iter = stats.getFactionRanks().find(info.mPcFaction); if (iter == stats.getFactionRanks().end()) return false; // check rank if (iter->second < info.mData.mPCrank) return false; } 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(mActor.getClass().getPrimaryFaction(mActor)); if (iter == stats.getFactionRanks().end()) return false; // check rank if (iter->second < info.mData.mPCrank) return false; } // check cell if (!info.mCell.empty()) { // supports partial matches, just like getPcCell 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 { 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 isCreature = (mActor.getType() != ESM::NPC::sRecordId); if (isCreature) return true; int actorDisposition = MWBase::Environment::get().getMechanicsManager()->getDerivedDisposition(mActor); // For service refusal, the disposition check is inverted. However, a value of 0 still means "always succeed". return invert ? (info.mData.mDisposition == 0 || actorDisposition < info.mData.mDisposition) : (actorDisposition >= info.mData.mDisposition); } bool MWDialogue::Filter::testFunctionLocal(const MWDialogue::SelectWrapper& select) const { const ESM::RefId& scriptName = mActor.getClass().getScript(mActor); if (scriptName.empty()) return false; // no script std::string name = select.getName(); const Compiler::Locals& localDefs = MWBase::Environment::get().getScriptManager()->getLocals(scriptName); char type = localDefs.getType(name); if (type == ' ') return false; // script does not have a variable of this 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 const MWScript::Locals& locals = mActor.getRefData().getLocals(); if (locals.isEmpty()) 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]); } throw std::logic_error("unknown local variable type in dialogue filter"); } bool MWDialogue::Filter::testSelectStruct(const SelectWrapper& select) const { if (select.isNpcOnly() && (mActor.getType() != ESM::NPC::sRecordId)) // If the actor is a creature, we pass all conditions only applicable to NPCs. return true; if (select.getFunction() == ESM::DialogueCondition::Function_Choice && mChoice == -1) // If not currently in a choice, we reject all conditions that test against choices. return false; 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. 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)); // We must not do the comparison for inverted functions (eg. Function_NotClass) case SelectWrapper::Type_Inverted: return getSelectStructBoolean(select); } return true; } bool MWDialogue::Filter::testSelectStructNumeric(const SelectWrapper& select) const { switch (select.getFunction()) { case ESM::DialogueCondition::Function_Global: // internally all globals are float :( return select.selectCompare(MWBase::Environment::get().getWorld()->getGlobalFloat(select.getName())); case ESM::DialogueCondition::Function_Local: { return testFunctionLocal(select); } case ESM::DialogueCondition::Function_NotLocal: { return !testFunctionLocal(select); } case ESM::DialogueCondition::Function_PcHealthPercent: { MWWorld::Ptr player = MWMechanics::getPlayer(); return select.selectCompare( static_cast(player.getClass().getCreatureStats(player).getHealth().getRatio() * 100)); } 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(); return select.selectCompare(value); } case ESM::DialogueCondition::Function_Health_Percent: { return select.selectCompare( static_cast(mActor.getClass().getCreatureStats(mActor).getHealth().getRatio() * 100)); } default: throw std::runtime_error("unknown numeric select function"); } } int MWDialogue::Filter::getSelectStructInteger(const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { case ESM::DialogueCondition::Function_Journal: return MWBase::Environment::get().getJournal()->getJournalIndex(select.getId()); case ESM::DialogueCondition::Function_Item: { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); return store.count(select.getId()); } case ESM::DialogueCondition::Function_Dead: return MWBase::Environment::get().getMechanicsManager()->countDeaths(select.getId()); case ESM::DialogueCondition::Function_Choice: return mChoice; 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"); } 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: { 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; } case ESM::DialogueCondition::Function_PcLevel: return player.getClass().getCreatureStats(player).getLevel(); case ESM::DialogueCondition::Function_PcGender: return player.get()->mBase->isMale() ? 0 : 1; case ESM::DialogueCondition::Function_PcClothingModifier: { 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 { MWWorld::ConstContainerStoreIterator slot = store.getSlot(i); if (slot != store.end()) value += slot->getClass().getValue(*slot); } return value; } case ESM::DialogueCondition::Function_PcCrimeLevel: return player.getClass().getNpcStats(player).getBounty(); case ESM::DialogueCondition::Function_RankRequirement: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; int rank = getFactionRank(player, faction); if (rank >= 9) return 0; // max rank int result = 0; if (hasFactionRankSkillRequirements(player, faction, rank + 1)) result += 1; if (hasFactionRankReputationRequirements(player, faction, rank + 1)) result += 2; return result; } case ESM::DialogueCondition::Function_Level: return mActor.getClass().getCreatureStats(mActor).getLevel(); case ESM::DialogueCondition::Function_PcReputation: return player.getClass().getNpcStats(player).getReputation(); case ESM::DialogueCondition::Function_Weather: return MWBase::Environment::get().getWorld()->getCurrentWeather(); case ESM::DialogueCondition::Function_Reputation: return mActor.getClass().getNpcStats(mActor).getReputation(); case ESM::DialogueCondition::Function_FactionRankDifference: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return 0; int rank = getFactionRank(player, faction); int npcRank = mActor.getClass().getPrimaryFactionRank(mActor); return rank - npcRank; } case ESM::DialogueCondition::Function_PcWerewolfKills: return player.getClass().getNpcStats(player).getWerewolfKills(); case ESM::DialogueCondition::Function_FacReactionLowest: case ESM::DialogueCondition::Function_FacReactionHighest: { bool low = select.getFunction() == ESM::DialogueCondition::Function_FacReactionLowest; const ESM::RefId& factionId = mActor.getClass().getPrimaryFaction(mActor); if (factionId.empty()) return 0; int value = 0; MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); std::map::const_iterator playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { int reaction = MWBase::Environment::get().getDialogueManager()->getFactionReaction( factionId, playerFactionIt->first); if (low ? reaction < value : reaction > value) value = reaction; } return value; } case ESM::DialogueCondition::Function_CreatureTarget: { MWWorld::Ptr target; mActor.getClass().getCreatureStats(mActor).getAiSequence().getCombatTarget(target); if (!target.isEmpty()) { 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"); } } bool MWDialogue::Filter::getSelectStructBoolean(const SelectWrapper& select) const { MWWorld::Ptr player = MWMechanics::getPlayer(); switch (select.getFunction()) { case ESM::DialogueCondition::Function_NotId: return mActor.getCellRef().getRefId() != select.getId(); case ESM::DialogueCondition::Function_NotFaction: return mActor.getClass().getPrimaryFaction(mActor) != select.getId(); case ESM::DialogueCondition::Function_NotClass: return mActor.get()->mBase->mClass != select.getId(); case ESM::DialogueCondition::Function_NotRace: return mActor.get()->mBase->mRace != select.getId(); 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); case ESM::DialogueCondition::Function_SameRace: return mActor.get()->mBase->mRace == player.get()->mBase->mRace; case ESM::DialogueCondition::Function_SameFaction: return player.getClass().getNpcStats(player).isInFaction(mActor.getClass().getPrimaryFaction(mActor)); case ESM::DialogueCondition::Function_PcCommonDisease: return player.getClass().getCreatureStats(player).hasCommonDisease(); case ESM::DialogueCondition::Function_PcBlightDisease: return player.getClass().getCreatureStats(player).hasBlightDisease(); case ESM::DialogueCondition::Function_PcCorprus: return player.getClass() .getCreatureStats(player) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Corprus) .getMagnitude() != 0; case ESM::DialogueCondition::Function_PcExpelled: { const ESM::RefId& faction = mActor.getClass().getPrimaryFaction(mActor); if (faction.empty()) return false; return player.getClass().getNpcStats(player).getExpelled(faction); } case ESM::DialogueCondition::Function_PcVampire: return player.getClass() .getCreatureStats(player) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Vampirism) .getMagnitude() > 0; case ESM::DialogueCondition::Function_TalkedToPc: return mTalkedToPlayer; case ESM::DialogueCondition::Function_Alarmed: return mActor.getClass().getCreatureStats(mActor).isAlarmed(); case ESM::DialogueCondition::Function_Detected: return MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, mActor); case ESM::DialogueCondition::Function_Attacked: return mActor.getClass().getCreatureStats(mActor).getAttacked(); case ESM::DialogueCondition::Function_ShouldAttack: return MWBase::Environment::get().getMechanicsManager()->isAggressive(mActor, MWMechanics::getPlayer()); case ESM::DialogueCondition::Function_Werewolf: return mActor.getClass().getNpcStats(mActor).isWerewolf(); default: throw std::runtime_error("unknown boolean select function"); } } int MWDialogue::Filter::getFactionRank(const MWWorld::Ptr& actor, const ESM::RefId& factionId) const { MWMechanics::NpcStats& stats = actor.getClass().getNpcStats(actor); return stats.getFactionRank(factionId); } bool MWDialogue::Filter::hasFactionRankSkillRequirements( const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const { if (!actor.getClass().getNpcStats(actor).hasSkillsForRank(factionId, rank)) return false; const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); 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 ESM::RefId& factionId, int rank) const { MWMechanics::NpcStats& stats = actor.getClass().getNpcStats(actor); const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); 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::Response MWDialogue::Filter::search( const ESM::Dialogue& dialogue, const bool fallbackToInfoRefusal) const { auto suitableInfos = list(dialogue, fallbackToInfoRefusal, false); if (suitableInfos.empty()) return {}; else return suitableInfos[0]; } bool MWDialogue::Filter::couldPotentiallyMatch(const ESM::DialInfo& info) const { return testActor(info) && matchesStaticFilters(info, mActor); } std::vector MWDialogue::Filter::list( const ESM::Dialogue& dialogue, bool fallbackToInfoRefusal, bool searchAll, bool invertDisposition) const { std::vector infos; bool infoRefusal = false; // Iterate over topic responses to find a matching one for (const auto& info : dialogue.mInfo) { if (testActor(info) && testPlayer(info) && testSelectStructs(info)) { if (testDisposition(info, invertDisposition)) { infos.emplace_back(&dialogue, &info); if (!searchAll) break; } else infoRefusal = true; } } if (infos.empty() && infoRefusal && fallbackToInfoRefusal) { // No response is valid because of low NPC disposition, // search a response in the topic "Info Refusal" const MWWorld::Store& dialogues = MWBase::Environment::get().getESMStore()->get(); const ESM::Dialogue& infoRefusalDialogue = *dialogues.find(ESM::RefId::stringRefId("Info Refusal")); for (const auto& info : infoRefusalDialogue.mInfo) if (testActor(info) && testPlayer(info) && testSelectStructs(info) && testDisposition(info, invertDisposition)) { infos.emplace_back(&infoRefusalDialogue, &info); if (!searchAll) break; } } return infos; } openmw-openmw-0.49.0/apps/openmw/mwdialogue/filter.hpp000066400000000000000000000052031503074453300230230ustar00rootroot00000000000000#ifndef GAME_MWDIALOGUE_FILTER_H #define GAME_MWDIALOGUE_FILTER_H #include #include #include "../mwworld/ptr.hpp" namespace ESM { struct DialInfo; struct Dialogue; class RefId; } namespace MWDialogue { class SelectWrapper; class Filter { MWWorld::Ptr mActor; int mChoice; bool mTalkedToPlayer; 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 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 testFunctionLocal(const SelectWrapper& select) const; bool testSelectStruct(const SelectWrapper& select) const; bool testSelectStructNumeric(const SelectWrapper& select) const; int getSelectStructInteger(const SelectWrapper& select) const; bool getSelectStructBoolean(const SelectWrapper& select) const; int getFactionRank(const MWWorld::Ptr& actor, const ESM::RefId& factionId) const; bool hasFactionRankSkillRequirements(const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const; bool hasFactionRankReputationRequirements( const MWWorld::Ptr& actor, const ESM::RefId& factionId, int rank) const; public: using Response = std::pair; 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. 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. 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. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/hypertextparser.cpp000066400000000000000000000051171503074453300250060ustar00rootroot00000000000000#include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/store.hpp" #include "keywordsearch.hpp" #include "hypertextparser.hpp" namespace MWDialogue { namespace HyperTextParser { std::vector parseHyperText(const std::string& text) { std::vector result; size_t pos_end = std::string::npos, iteration_pos = 0; for (;;) { size_t pos_begin = text.find('@', iteration_pos); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { if (pos_begin != iteration_pos) tokenizeKeywords(text.substr(iteration_pos, pos_begin - iteration_pos), result); std::string link = text.substr(pos_begin + 1, pos_end - pos_begin - 1); result.emplace_back(link, Token::ExplicitLink); iteration_pos = pos_end + 1; } else { if (iteration_pos != text.size()) tokenizeKeywords(text.substr(iteration_pos), result); break; } } return result; } void tokenizeKeywords(const std::string& text, std::vector& tokens) { const auto& keywordSearch = MWBase::Environment::get().getESMStore()->get().getDialogIdKeywordSearch(); std::vector::Match> matches; keywordSearch.highlightKeywords(text.begin(), text.end(), matches); for (const auto& match : matches) { tokens.emplace_back(std::string(match.mBeg, match.mEnd), Token::ImplicitKeyword); } } size_t removePseudoAsterisks(std::string& phrase) { size_t pseudoAsterisksCount = 0; if (!phrase.empty()) { std::string::reverse_iterator rit = phrase.rbegin(); const char specialPseudoAsteriskCharacter = 127; while (rit != phrase.rend() && *rit == specialPseudoAsteriskCharacter) { pseudoAsterisksCount++; ++rit; } } phrase = phrase.substr(0, phrase.length() - pseudoAsterisksCount); return pseudoAsterisksCount; } } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/hypertextparser.hpp000066400000000000000000000017201503074453300250070ustar00rootroot00000000000000#ifndef GAME_MWDIALOGUE_HYPERTEXTPARSER_H #define GAME_MWDIALOGUE_HYPERTEXTPARSER_H #include #include namespace MWDialogue { namespace HyperTextParser { struct Token { enum Type { ExplicitLink, // enclosed in @# ImplicitKeyword }; Token(const std::string& text, Type type) : mText(text) , mType(type) { } bool isExplicitLink() { return mType == ExplicitLink; } std::string mText; Type mType; }; // 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); } } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/journalentry.cpp000066400000000000000000000106431503074453300242710ustar00rootroot00000000000000#include "journalentry.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/globals.hpp" #include "../mwscript/interpretercontext.hpp" namespace MWDialogue { Entry::Entry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor) : mInfoId(infoId) { 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->mId == mInfoId) { if (actor.isEmpty()) { MWScript::InterpreterContext interpreterContext(nullptr, MWWorld::Ptr()); mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); } else { MWScript::InterpreterContext interpreterContext(&actor.getRefData().getLocals(), actor); mText = Interpreter::fixDefinesDialog(iter->mResponse, interpreterContext); } return; } 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) { } const std::string& Entry::getText() const { return mText; } 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(const ESM::JournalEntry& record) : Entry(record) , mTopic(record.mTopic) { } void JournalEntry::write(ESM::JournalEntry& entry) const { Entry::write(entry); entry.mTopic = mTopic; } JournalEntry JournalEntry::makeFromQuest(const ESM::RefId& topic, int index) { return JournalEntry(topic, idFromIndex(topic, index), MWWorld::Ptr()); } const ESM::RefId& JournalEntry::idFromIndex(const ESM::RefId& topic, int index) { 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) { return iter->mId; } throw std::runtime_error("unknown journal index for topic " + topic.toDebugString()); } StampedJournalEntry::StampedJournalEntry() : mDay(0) , mMonth(0) , mDayOfMonth(0) { } 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) { } void StampedJournalEntry::write(ESM::JournalEntry& entry) const { JournalEntry::write(entry); entry.mDay = mDay; entry.mMonth = mMonth; entry.mDayOfMonth = mDayOfMonth; } StampedJournalEntry StampedJournalEntry::makeFromQuest( const ESM::RefId& topic, int index, const MWWorld::Ptr& actor) { 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); } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/journalentry.hpp000066400000000000000000000035701503074453300242770ustar00rootroot00000000000000#ifndef GAME_MWDIALOGUE_JOURNALENTRY_H #define GAME_MWDIALOGUE_JOURNALENTRY_H #include #include #include namespace ESM { struct JournalEntry; } namespace MWWorld { class Ptr; } namespace MWDialogue { /// \brief Basic quest/dialogue/topic entry struct Entry { ESM::RefId mInfoId; std::string mText; std::string mActorName; // optional Entry() = default; /// actor is optional Entry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor); Entry(const ESM::JournalEntry& record); const std::string& getText() const; void write(ESM::JournalEntry& entry) const; }; /// \brief A dialogue entry /// /// Same as entry, but store TopicID struct JournalEntry : public Entry { ESM::RefId mTopic; JournalEntry() = default; JournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, const MWWorld::Ptr& actor); JournalEntry(const ESM::JournalEntry& record); void write(ESM::JournalEntry& entry) const; static JournalEntry makeFromQuest(const ESM::RefId& topic, int index); static const ESM::RefId& idFromIndex(const ESM::RefId& topic, int index); }; /// \brief A quest entry with a timestamp. struct StampedJournalEntry : public JournalEntry { int mDay; int mMonth; int mDayOfMonth; StampedJournalEntry(); StampedJournalEntry(const ESM::RefId& topic, const ESM::RefId& infoId, int day, int month, int dayOfMonth, const MWWorld::Ptr& actor); StampedJournalEntry(const ESM::JournalEntry& record); void write(ESM::JournalEntry& entry) const; static StampedJournalEntry makeFromQuest(const ESM::RefId& topic, int index, const MWWorld::Ptr& actor); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/journalimp.cpp000066400000000000000000000211641503074453300237150ustar00rootroot00000000000000#include "journalimp.hpp" #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" namespace MWDialogue { Quest& Journal::getOrStartQuest(const ESM::RefId& id) { TQuestContainer::iterator iter = mQuests.find(id); if (iter == mQuests.end()) iter = mQuests.emplace(id, Quest(id)).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); } Topic& Journal::getTopic(const ESM::RefId& id) { TTopicContainer::iterator iter = mTopics.find(id); if (iter == mTopics.end()) { std::pair result = mTopics.insert(std::make_pair(id, Topic(id))); iter = result.first; } return iter->second; } bool Journal::isThere(const ESM::RefId& topicId, const ESM::RefId& infoId) const { 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) if (iter->mId == infoId) return true; } return false; } Journal::Journal() {} void Journal::clear() { mJournal.clear(); mQuests.clear(); mTopics.clear(); } void Journal::addEntry(const ESM::RefId& id, int index, const MWWorld::Ptr& actor) { // bail out if we already have heard this... 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}"); } return; } StampedJournalEntry entry = StampedJournalEntry::makeFromQuest(id, index, actor); 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}"); } } void Journal::setJournalIndex(const ESM::RefId& id, int index) { Quest& quest = getOrStartQuest(id); quest.setIndex(index); } void Journal::addTopic(const ESM::RefId& topicId, const ESM::RefId& infoId, const MWWorld::Ptr& actor) { Topic& topic = getTopic(topicId); JournalEntry entry(topicId, infoId, actor); entry.mActorName = actor.getClass().getName(actor); topic.addEntry(entry); } void Journal::removeLastAddedTopicResponse(const ESM::RefId& topicId, std::string_view actorName) { Topic& topic = getTopic(topicId); topic.removeLastAddedResponse(actorName); if (topic.begin() == topic.end()) mTopics.erase(mTopics.find(topicId)); // All responses removed -> remove topic } int Journal::getJournalIndex(const ESM::RefId& id) const { TQuestContainer::const_iterator iter = mQuests.find(id); if (iter == mQuests.end()) return 0; return iter->second.getIndex(); } Journal::TEntryIter Journal::begin() const { return mJournal.begin(); } Journal::TEntryIter Journal::end() const { return mJournal.end(); } Journal::TQuestIter Journal::questBegin() const { return mQuests.begin(); } Journal::TQuestIter Journal::questEnd() const { return mQuests.end(); } Journal::TTopicIter Journal::topicBegin() const { return mTopics.begin(); } Journal::TTopicIter Journal::topicEnd() const { return mTopics.end(); } int Journal::countSavedGameRecords() const { int count = static_cast(mQuests.size()); 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()); 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 { 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); 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); } } 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); } for (TTopicIter iter(mTopics.begin()); iter != mTopics.end(); ++iter) { const Topic& topic = iter->second; 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); } } } void Journal::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_JOUR) { ESM::JournalEntry record; record.load(reader); if (isThere(record.mTopic, record.mInfo)) switch (record.mType) { case ESM::JournalEntry::Type_Quest: getOrStartQuest(record.mTopic).insertEntry(record); break; case ESM::JournalEntry::Type_Journal: mJournal.push_back(record); break; case ESM::JournalEntry::Type_Topic: getTopic(record.mTopic).insertEntry(record); break; } } else if (type == ESM::REC_QUES) { ESM::QuestState record; record.load(reader); if (isThere(record.mTopic)) { 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); } } } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/journalimp.hpp000066400000000000000000000054011503074453300237160ustar00rootroot00000000000000#ifndef GAME_MWDIALOG_JOURNAL_H #define GAME_MWDIALOG_JOURNAL_H #include "../mwbase/journal.hpp" #include "quest.hpp" namespace MWDialogue { /// \brief The player's journal class Journal : public MWBase::Journal { TEntryContainer mJournal; TQuestContainer mQuests; TTopicContainer mTopics; private: Topic& getTopic(const ESM::RefId& id); bool isThere(const ESM::RefId& topicId, const ESM::RefId& infoId = ESM::RefId()) const; public: Journal(); void clear() override; Quest* getQuestOrNull(const ESM::RefId& id) override; ///< Gets a pointer to the requested quest. Will return nullptr if the quest has not been started. 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 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 setJournalIndex(const ESM::RefId& id, int index) override; ///< Set the journal index without adding an entry. int getJournalIndex(const ESM::RefId& 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 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 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. TQuestIter questBegin() const override; ///< Iterator pointing to the first quest (sorted by topic ID) 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 topicEnd() const override; ///< Iterator pointing past the last topic. int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) const override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/keywordsearch.hpp000066400000000000000000000204551503074453300244160ustar00rootroot00000000000000#ifndef GAME_MWDIALOGUE_KEYWORDSEARCH_H #define GAME_MWDIALOGUE_KEYWORDSEARCH_H #include // std::reverse #include #include #include #include #include #include namespace MWDialogue { template class KeywordSearch { public: using Point = std::string::const_iterator; struct Match { Point mBeg; Point mEnd; value_t mValue; }; void seed(std::string_view keyword, value_t value) { if (keyword.empty()) return; seed_impl(keyword, std::move(value), 0, mRoot); } void clear() { mRoot.mChildren.clear(); mRoot.mKeyword.clear(); } bool containsKeyword(std::string_view keyword, value_t& value) { auto it = keyword.begin(); auto current = mRoot.mChildren.find(Misc::StringUtils::toLower(*it)); if (current == mRoot.mChildren.end()) return false; else if (Misc::StringUtils::ciEqual(current->second.mKeyword, keyword)) { value = current->second.mValue; return true; } 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; } static bool sortMatches(const Match& left, const Match& right) { return left.mBeg < right.mBeg; } void highlightKeywords(Point beg, Point end, std::vector& out) const { std::vector matches; for (Point i = beg; i != end; ++i) { // 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; // see how far the match goes Point j = i; // some keywords might be longer variations of other keywords, so we definitely need a list of // candidates the first element in the pair is length of the match, i.e. depth from the first character // on std::vector> candidates; while ((j + 1) != end) { typename Entry::childen_t::const_iterator next = candidate->second.mChildren.find(Misc::StringUtils::toLower(*++j)); if (next == candidate->second.mChildren.end()) { if (candidate->second.mKeyword.size() > 0) candidates.push_back(std::make_pair((j - i), candidate)); break; } candidate = next; 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()); for (const auto& [pos, c] : candidates) { 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; } } // resolve overlapping keywords while (!matches.empty()) { std::size_t longestKeywordSize = 0; typename std::vector::iterator longestKeyword = matches.begin(); for (typename std::vector::iterator it = matches.begin(); it != matches.end(); ++it) { std::size_t size = it->mEnd - it->mBeg; if (size > longestKeywordSize) { longestKeywordSize = size; longestKeyword = it; } typename std::vector::iterator next = it; ++next; if (next == matches.end()) break; 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();) { if (it->mBeg < keyword.mEnd && it->mEnd > keyword.mBeg) it = matches.erase(it); else ++it; } } std::sort(out.begin(), out.end(), sortMatches); } private: struct Entry { typedef std::map childen_t; std::string mKeyword; value_t mValue; childen_t mChildren; }; 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); if (j == entry.mChildren.end()) { entry.mChildren[ch].mValue = std::move(value); entry.mChildren[ch].mKeyword = keyword; } else { if (j->second.mKeyword.size() > 0) { if (keyword == j->second.mKeyword) throw std::runtime_error("duplicate keyword inserted"); const auto& pushKeyword = j->second.mKeyword; if (depth >= pushKeyword.size()) throw std::runtime_error("unexpected"); 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); } } Entry mRoot; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/quest.cpp000066400000000000000000000055501503074453300226770ustar00rootroot00000000000000#include "quest.hpp" #include #include #include "../mwbase/luamanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" namespace MWDialogue { Quest::Quest() : 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) { } std::string_view Quest::getName() const { 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) return iter->mResponse; return {}; } int Quest::getIndex() const { return mIndex; } 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; } bool Quest::isFinished() const { return mFinished; } void Quest::setFinished(bool finished) { mFinished = finished; } bool Quest::addEntry(const JournalEntry& entry) { const ESM::Dialogue* dialogue = MWBase::Environment::get().getESMStore()->get().find(entry.mTopic); auto info = std::find_if(dialogue->mInfo.begin(), dialogue->mInfo.end(), [&](const auto& info) { return info.mId == entry.mInfoId; }); if (info == dialogue->mInfo.end() || info->mData.mJournalIndex == -1) throw std::runtime_error("unknown journal entry for topic " + mTopic.toDebugString()); 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) { mIndex = info->mData.mJournalIndex; MWBase::Environment::get().getLuaManager()->questUpdated(mTopic, mIndex); } 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 return info->mQuestStatus == ESM::DialInfo::QS_Restart; } void Quest::write(ESM::QuestState& state) const { state.mTopic = getTopic(); state.mState = mIndex; state.mFinished = mFinished; } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/quest.hpp000066400000000000000000000020171503074453300226770ustar00rootroot00000000000000#ifndef GAME_MWDIALOG_QUEST_H #define GAME_MWDIALOG_QUEST_H #include "topic.hpp" namespace ESM { struct QuestState; } namespace MWDialogue { /// \brief A quest in progress or a completed quest class Quest : public Topic { int mIndex; bool mFinished; public: Quest(); Quest(const ESM::RefId& topic); Quest(const ESM::QuestState& state); std::string_view getName() const override; ///< May be an empty string int getIndex() const; void setIndex(int index); ///< Calling this function with a non-existent index will throw an exception. bool isFinished() const; void setFinished(bool finished); 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 write(ESM::QuestState& state) const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/scripttest.cpp000066400000000000000000000136571503074453300237510ustar00rootroot00000000000000#include "scripttest.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwscript/compilercontext.hpp" #include #include #include #include #include #include #include #include #include "filter.hpp" #include #include #include namespace { 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) { bool success = true; ++total; try { std::istringstream input(info.mResultScript + "\n"); Compiler::Scanner scanner(errorHandler, input, extensions); Compiler::Locals locals; 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); } Compiler::ScriptParser parser(errorHandler, context, locals, false); scanner.scan(parser); 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; } return success; } 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) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/scripttest.hpp000066400000000000000000000007461503074453300237510ustar00rootroot00000000000000#ifndef OPENMW_MWDIALOGUE_SCRIPTTEST_H #define OPENMW_MWDIALOGUE_SCRIPTTEST_H #include namespace MWDialogue { 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); } } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/selectwrapper.cpp000066400000000000000000000273121503074453300244160ustar00rootroot00000000000000#include "selectwrapper.hpp" #include #include #include #include #include namespace { template bool selectCompareImp(ESM::DialogueCondition::Comparison comp, T1 value1, T2 value2) { switch (comp) { case ESM::DialogueCondition::Comp_Eq: return value1 == value2; case ESM::DialogueCondition::Comp_Ne: return value1 != value2; case ESM::DialogueCondition::Comp_Gt: 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"); } } template bool selectCompareImp(const ESM::DialogueCondition& select, T value1) { return std::visit( [&](auto value) { return selectCompareImp(select.mComparison, value1, value); }, select.mValue); } } MWDialogue::SelectWrapper::SelectWrapper(const ESM::DialogueCondition& select) : mSelect(select) { } ESM::DialogueCondition::Function MWDialogue::SelectWrapper::getFunction() const { return mSelect.mFunction; } int MWDialogue::SelectWrapper::getArgument() const { switch (mSelect.mFunction) { // AI settings 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 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 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 ESM::DialogueCondition::Function_PcMagicka: return 1; case ESM::DialogueCondition::Function_PcFatigue: return 2; case ESM::DialogueCondition::Function_PcHealth: return 0; default: return 0; } } MWDialogue::SelectWrapper::Type MWDialogue::SelectWrapper::getType() const { switch (mSelect.mFunction) { 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; 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; 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; 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; default: return Type_None; }; } bool MWDialogue::SelectWrapper::isNpcOnly() const { switch (mSelect.mFunction) { case ESM::DialogueCondition::Function_NotFaction: case ESM::DialogueCondition::Function_NotClass: case ESM::DialogueCondition::Function_NotRace: case ESM::DialogueCondition::Function_SameSex: case ESM::DialogueCondition::Function_SameRace: case ESM::DialogueCondition::Function_SameFaction: case ESM::DialogueCondition::Function_RankRequirement: case ESM::DialogueCondition::Function_Reputation: case ESM::DialogueCondition::Function_FactionRankDifference: case ESM::DialogueCondition::Function_Werewolf: case ESM::DialogueCondition::Function_PcWerewolfKills: case ESM::DialogueCondition::Function_FacReactionLowest: case ESM::DialogueCondition::Function_FacReactionHighest: return true; default: return false; } } bool MWDialogue::SelectWrapper::selectCompare(int value) const { return selectCompareImp(mSelect, value); } bool MWDialogue::SelectWrapper::selectCompare(float value) const { return selectCompareImp(mSelect, value); } bool MWDialogue::SelectWrapper::selectCompare(bool value) const { return selectCompareImp(mSelect, static_cast(value)); } std::string MWDialogue::SelectWrapper::getName() const { 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); } openmw-openmw-0.49.0/apps/openmw/mwdialogue/selectwrapper.hpp000066400000000000000000000020211503074453300244110ustar00rootroot00000000000000#ifndef GAME_MWDIALOGUE_SELECTWRAPPER_H #define GAME_MWDIALOGUE_SELECTWRAPPER_H #include namespace MWDialogue { class SelectWrapper { const ESM::DialogueCondition& mSelect; public: enum Type { Type_None, Type_Integer, Type_Numeric, Type_Boolean, Type_Inverted }; public: SelectWrapper(const ESM::DialogueCondition& select); ESM::DialogueCondition::Function getFunction() const; int getArgument() const; Type getType() const; bool isNpcOnly() const; ///< \attention Do not call any of the select functions for this select struct! bool selectCompare(int value) const; bool selectCompare(float value) const; bool selectCompare(bool value) const; std::string getName() const; ///< Return case-smashed name. std::string_view getCellName() const; ESM::RefId getId() const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwdialogue/topic.cpp000066400000000000000000000032361503074453300226530ustar00rootroot00000000000000#include "topic.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" namespace MWDialogue { Topic::Topic() {} Topic::Topic(const ESM::RefId& topic) : mTopic(topic) , mName(MWBase::Environment::get().getESMStore()->get().find(topic)->mStringId) { } Topic::~Topic() {} bool Topic::addEntry(const JournalEntry& entry) { 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 false; } mEntries.push_back(entry); // we want slicing here return false; } void Topic::insertEntry(const ESM::JournalEntry& entry) { mEntries.push_back(entry); } const ESM::RefId& Topic::getTopic() const { return mTopic; } std::string_view Topic::getName() const { return mName; } Topic::TEntryIter Topic::begin() const { return mEntries.begin(); } Topic::TEntryIter Topic::end() const { return mEntries.end(); } void Topic::removeLastAddedResponse(std::string_view actorName) { 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 return; } } } } openmw-openmw-0.49.0/apps/openmw/mwdialogue/topic.hpp000066400000000000000000000024751503074453300226640ustar00rootroot00000000000000#ifndef GAME_MWDIALOG_TOPIC_H #define GAME_MWDIALOG_TOPIC_H #include #include #include #include "journalentry.hpp" namespace ESM { struct JournalEntry; } namespace MWDialogue { /// \brief Collection of seen responses for a topic class Topic { public: typedef std::vector TEntryContainer; typedef TEntryContainer::const_iterator TEntryIter; protected: ESM::RefId mTopic; std::string mName; TEntryContainer mEntries; public: Topic(); Topic(const ESM::RefId& topic); virtual ~Topic(); virtual bool addEntry(const JournalEntry& entry); ///< Add entry /// /// \note Redundant entries are ignored. void insertEntry(const ESM::JournalEntry& entry); ///< Add entry without checking for redundant entries or modifying the state of the /// topic otherwise const ESM::RefId& getTopic() const; virtual std::string_view getName() const; void removeLastAddedResponse(std::string_view 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. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/000077500000000000000000000000001503074453300200205ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwgui/alchemywindow.cpp000066400000000000000000000467361503074453300234160ustar00rootroot00000000000000#include "alchemywindow.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/alchemy.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include #include "inventoryitemmodel.hpp" #include "itemview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" #include "widgets.hpp" namespace MWGui { AlchemyWindow::AlchemyWindow() : WindowBase("openmw_alchemy_window.layout") , mCurrentFilter(FilterType::ByName) , mModel(nullptr) , mSortModel(nullptr) , mAlchemy(std::make_unique()) , mApparatus(4) , mIngredients(4) { getWidget(mCreateButton, "CreateButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mIngredients[0], "Ingredient1"); getWidget(mIngredients[1], "Ingredient2"); getWidget(mIngredients[2], "Ingredient3"); getWidget(mIngredients[3], "Ingredient4"); getWidget(mApparatus[0], "Apparatus1"); getWidget(mApparatus[1], "Apparatus2"); getWidget(mApparatus[2], "Apparatus3"); getWidget(mApparatus[3], "Apparatus4"); getWidget(mEffectsBox, "CreatedEffects"); getWidget(mBrewCountEdit, "BrewCount"); getWidget(mIncreaseButton, "IncreaseButton"); getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mNameEdit, "NameEdit"); getWidget(mItemView, "ItemView"); getWidget(mFilterValue, "FilterValue"); getWidget(mFilterType, "FilterType"); mBrewCountEdit->eventValueChanged += MyGUI::newDelegate(this, &AlchemyWindow::onCountValueChanged); mBrewCountEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); mBrewCountEdit->setMinValue(1); mBrewCountEdit->setValue(1); mIncreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &AlchemyWindow::onIncreaseButtonPressed); mIncreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &AlchemyWindow::onCountButtonReleased); mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &AlchemyWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &AlchemyWindow::onCountButtonReleased); mItemView->eventItemClicked += MyGUI::newDelegate(this, &AlchemyWindow::onSelectedItem); mIngredients[0]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); mIngredients[1]->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::onIngredientSelected); 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); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &AlchemyWindow::onAccept); mFilterValue->eventComboChangePosition += MyGUI::newDelegate(this, &AlchemyWindow::onFilterChanged); mFilterValue->eventEditTextChange += MyGUI::newDelegate(this, &AlchemyWindow::onFilterEdited); mFilterType->eventMouseButtonClick += MyGUI::newDelegate(this, &AlchemyWindow::switchFilterType); center(); } void AlchemyWindow::onAccept(MyGUI::EditBox* sender) { onCreateButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void AlchemyWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Alchemy); } void AlchemyWindow::onCreateButtonClicked(MyGUI::Widget* _sender) { mAlchemy->setPotionName(mNameEdit->getCaption()); int count = mAlchemy->countPotionsToBrew(); count = std::min(count, mBrewCountEdit->getValue()); createPotions(count); } void AlchemyWindow::createPotions(int count) { MWMechanics::Alchemy::Result result = mAlchemy->create(mNameEdit->getCaption(), count); 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(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 (size_t i = 0; i < mIngredients.size(); ++i) if (mIngredients[i]->isUserString("ToolTipType")) { MWWorld::Ptr ingred = *mIngredients[i]->getUserData(); if (ingred.getCellRef().getCount() == 0) mAlchemy->removeIngredient(i); } updateFilters(); update(); } void AlchemyWindow::initFilter() { auto const& wm = MWBase::Environment::get().getWindowManager(); std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); if (mFilterType->getCaption() == ingredient) mCurrentFilter = FilterType::ByName; else mCurrentFilter = FilterType::ByEffect; updateFilters(); mFilterValue->clearIndexSelected(); updateFilters(); } void AlchemyWindow::switchFilterType(MyGUI::Widget* _sender) { auto const& wm = MWBase::Environment::get().getWindowManager(); std::string_view ingredient = wm->getGameSettingString("sIngredients", "Ingredients"); auto* button = _sender->castType(); if (button->getCaption() == ingredient) { button->setCaption(MyGUI::UString(wm->getGameSettingString("sMagicEffects", "Magic Effects"))); mCurrentFilter = FilterType::ByEffect; } else { button->setCaption(MyGUI::UString(ingredient)); mCurrentFilter = FilterType::ByName; } mSortModel->setNameFilter({}); mSortModel->setEffectFilter({}); mFilterValue->clearIndexSelected(); updateFilters(); mItemView->update(); } void AlchemyWindow::updateFilters() { std::set itemNames, itemEffects; for (size_t i = 0; i < mModel->getItemCount(); ++i) { MWWorld::Ptr item = mModel->getItem(i).mBase; if (item.getType() != ESM::Ingredient::sRecordId) continue; itemNames.emplace(item.getClass().getName(item)); 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); itemEffects.insert(effects.begin(), effects.end()); } mFilterValue->removeAllItems(); 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; } } void AlchemyWindow::applyFilter(const std::string& filter) { switch (mCurrentFilter) { case FilterType::ByName: mSortModel->setNameFilter(filter); break; case FilterType::ByEffect: mSortModel->setEffectFilter(filter); break; } mItemView->update(); } void AlchemyWindow::onFilterChanged(MyGUI::ComboBox* _sender, size_t _index) { // ignore spurious event fired when one edit the content after selection. // onFilterEdited will handle it. if (_index != MyGUI::ITEM_NONE) applyFilter(_sender->getItemNameAt(_index)); } void AlchemyWindow::onFilterEdited(MyGUI::EditBox* _sender) { applyFilter(_sender->getCaption()); } void AlchemyWindow::onOpen() { mAlchemy->clear(); mAlchemy->setAlchemist(MWMechanics::getPlayer()); 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(std::move(sortModel)); mItemView->resetScrollBars(); mNameEdit->setCaption({}); mBrewCountEdit->setValue(1); size_t index = 0; for (auto iter = mAlchemy->beginTools(); iter != mAlchemy->endTools() && index < mApparatus.size(); ++iter, ++index) { const auto& widget = mApparatus[index]; widget->setItem(*iter); widget->clearUserStrings(); if (!iter->isEmpty()) { widget->setUserString("ToolTipType", "ItemPtr"); widget->setUserData(MWWorld::Ptr(*iter)); } } update(); initFilter(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } void AlchemyWindow::onIngredientSelected(MyGUI::Widget* _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(); } void AlchemyWindow::onSelectedItem(int index) { MWWorld::Ptr item = mSortModel->getItem(index).mBase; int res = mAlchemy->addIngredient(item); if (res != -1) { update(); const ESM::RefId& sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } } void AlchemyWindow::update() { std::string suggestedName = mAlchemy->suggestPotionName(); if (suggestedName != mSuggestedPotionName) { mNameEdit->setCaptionWithReplacing(suggestedName); mSuggestedPotionName = std::move(suggestedName); } mSortModel->clearDragItems(); MWMechanics::Alchemy::TIngredientsIterator it = mAlchemy->beginIngredients(); for (int i = 0; i < 4; ++i) { ItemWidget* ingredient = mIngredients[i]; MWWorld::Ptr item; if (it != mAlchemy->endIngredients()) { item = *it; ++it; } if (!item.isEmpty()) mSortModel->addDragItem(item, item.getCellRef().getCount()); if (ingredient->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(ingredient->getChildAt(0)); ingredient->clearUserStrings(); ingredient->setItem(item); if (item.isEmpty()) continue; ingredient->setUserString("ToolTipType", "ItemPtr"); ingredient->setUserData(MWWorld::Ptr(item)); ingredient->setCount(item.getCellRef().getCount()); } mItemView->update(); std::vector effectIds = mAlchemy->listEffects(); Widgets::SpellEffectList list; unsigned int effectIndex = 0; for (const MWMechanics::EffectKey& effectKey : effectIds) { Widgets::SpellEffectParams params; params.mEffectID = 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) params.mAttribute = effectKey.mArg; params.mIsConstant = true; params.mNoTarget = true; params.mNoMagnitude = true; params.mKnown = mAlchemy->knownEffect(effectIndex, MWBase::Environment::get().getWorld()->getPlayerPtr()); list.push_back(params); ++effectIndex; } while (mEffectsBox->getChildCount()) 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); effectsWidget->setEffectList(list); std::vector effectItems; effectsWidget->createEffectWidgets(effectItems, mEffectsBox, coord, false, 0); effectsWidget->setCoord(coord); } void AlchemyWindow::addRepeatController(MyGUI::Widget* widget) { 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); } void AlchemyWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onIncreaseButtonTriggered(); } void AlchemyWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onDecreaseButtonTriggered(); } void AlchemyWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) { if (widget == mIncreaseButton) onIncreaseButtonTriggered(); else if (widget == mDecreaseButton) onDecreaseButtonTriggered(); } void AlchemyWindow::onCountButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { MyGUI::ControllerManager::getInstance().removeItem(_sender); } void AlchemyWindow::onCountValueChanged(int value) { mBrewCountEdit->setValue(std::abs(value)); } void AlchemyWindow::onIncreaseButtonTriggered() { int currentCount = mBrewCountEdit->getValue(); // prevent overflows if (currentCount == std::numeric_limits::max()) return; mBrewCountEdit->setValue(currentCount + 1); } void AlchemyWindow::onDecreaseButtonTriggered() { int currentCount = mBrewCountEdit->getValue(); if (currentCount > 1) mBrewCountEdit->setValue(currentCount - 1); } } openmw-openmw-0.49.0/apps/openmw/mwgui/alchemywindow.hpp000066400000000000000000000060021503074453300234010ustar00rootroot00000000000000#ifndef MWGUI_ALCHEMY_H #define MWGUI_ALCHEMY_H #include #include #include #include #include #include #include "itemselection.hpp" #include "windowbase.hpp" #include "../mwmechanics/alchemy.hpp" namespace MWGui { class ItemView; class ItemWidget; class InventoryItemModel; class SortFilterItemModel; class AlchemyWindow : public WindowBase { public: AlchemyWindow(); void onOpen() override; void onResChange(int, int) override { center(); } 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 }; FilterType mCurrentFilter; std::unique_ptr mItemSelectionDialog; ItemView* mItemView; InventoryItemModel* mModel; SortFilterItemModel* mSortModel; MyGUI::Button* mCreateButton; MyGUI::Button* mCancelButton; MyGUI::Widget* mEffectsBox; MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; Gui::AutoSizedButton* mFilterType; MyGUI::ComboBox* mFilterValue; MyGUI::EditBox* mNameEdit; Gui::NumericEditBox* mBrewCountEdit; 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); void onCountButtonReleased(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id); void onCountValueChanged(int value); void onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller); void applyFilter(const std::string& filter); void initFilter(); void onFilterChanged(MyGUI::ComboBox* _sender, size_t _index); void onFilterEdited(MyGUI::EditBox* _sender); void switchFilterType(MyGUI::Widget* _sender); void updateFilters(); void addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); void onSelectedItem(int index); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); void createPotions(int count); void update(); std::unique_ptr mAlchemy; std::vector mApparatus; std::vector mIngredients; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/backgroundimage.cpp000066400000000000000000000031111503074453300236420ustar00rootroot00000000000000#include "backgroundimage.hpp" #include namespace MWGui { void BackgroundImage::setBackgroundImage(const std::string& image, bool fixedRatio, bool stretch) { 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); } } 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(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/backgroundimage.hpp000066400000000000000000000017411503074453300236560ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_BACKGROUNDIMAGE_H #define OPENMW_MWGUI_BACKGROUNDIMAGE_H #include namespace MWGui { /** * @brief A variant of MyGUI::ImageBox with aspect ratio correction using black bars */ class BackgroundImage final : public MyGUI::ImageBox { MYGUI_RTTI_DERIVED(BackgroundImage) public: 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 setSize(const MyGUI::IntSize& _value) override; void setCoord(const MyGUI::IntCoord& _value) override; private: MyGUI::ImageBox* mChild; double mAspect; void adjustSize(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/birth.cpp000066400000000000000000000222321503074453300216350ustar00rootroot00000000000000#include "birth.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "widgets.hpp" namespace { bool sortBirthSigns(const std::pair& left, const std::pair& right) { return left.second->mName.compare(right.second->mName) < 0; } } namespace MWGui { BirthDialog::BirthDialog() : WindowModal("openmw_chargen_birth.layout") { // Centre dialog center(); getWidget(mSpellArea, "SpellArea"); getWidget(mBirthImage, "BirthsignImage"); getWidget(mBirthList, "BirthsignList"); mBirthList->setScrollVisible(true); mBirthList->eventListSelectAccept += MyGUI::newDelegate(this, &BirthDialog::onAccept); mBirthList->eventListChangePosition += MyGUI::newDelegate(this, &BirthDialog::onSelectBirth); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BirthDialog::onOkClicked); updateBirths(); updateSpells(); } void BirthDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void BirthDialog::onOpen() { WindowModal::onOpen(); updateBirths(); updateSpells(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mBirthList); // Show the current birthsign by default const auto& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) setBirthId(signId); } 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 (*mBirthList->getItemDataAt(i) == birthId) { mBirthList->setIndexSelected(i); break; } } updateSpells(); } // widget controls void BirthDialog::onOkClicked(MyGUI::Widget* _sender) { if (mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void BirthDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectBirth(_sender, _index); if (mBirthList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void BirthDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void BirthDialog::onSelectBirth(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; const ESM::RefId& birthId = *mBirthList->getItemDataAt(_index); if (mCurrentBirthId == birthId) return; mCurrentBirthId = birthId; updateSpells(); } // update widget content void BirthDialog::updateBirths() { mBirthList->removeAllItems(); const MWWorld::Store& signs = MWBase::Environment::get().getESMStore()->get(); // sort by name std::vector> birthSigns; for (const ESM::BirthSign& sign : signs) { birthSigns.emplace_back(sign.mId, &sign); } std::sort(birthSigns.begin(), birthSigns.end(), sortBirthSigns); int index = 0; for (auto& birthsignPair : birthSigns) { mBirthList->addItem(birthsignPair.second->mName, birthsignPair.first); if (mCurrentBirthId.empty()) { mBirthList->setIndexSelected(index); mCurrentBirthId = birthsignPair.first; } else if (birthsignPair.first == mCurrentBirthId) { mBirthList->setIndexSelected(index); } index++; } } void BirthDialog::updateSpells() { for (MyGUI::Widget* widget : mSpellItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSpellItems.clear(); if (mCurrentBirthId.empty()) return; Widgets::MWSpellPtr spellWidget; const int lineHeight = Settings::gui().mFontSize + 2; MyGUI::IntCoord coord(0, 0, mSpellArea->getWidth(), lineHeight); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::BirthSign* birth = store.get().find(mCurrentBirthId); mBirthImage->setImageTexture(Misc::ResourceHelpers::correctTexturePath( birth->mTexture, MWBase::Environment::get().getResourceSystem()->getVFS())); std::vector abilities, powers, spells; std::vector::const_iterator it = birth->mPowers.mList.begin(); std::vector::const_iterator end = birth->mPowers.mList.end(); for (; it != end; ++it) { 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); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Ability && type != ESM::Spell::ST_Power) continue; // We only want spell, ability and powers. if (type == ESM::Spell::ST_Ability) abilities.push_back(spellId); else if (type == ESM::Spell::ST_Power) powers.push_back(spellId); else if (type == ESM::Spell::ST_Spell) spells.push_back(spellId); } int i = 0; struct { const std::vector& spells; std::string_view label; } categories[3] = { { abilities, "sBirthsignmenu1" }, { powers, "sPowers" }, { spells, "sBirthsignmenu2" } }; for (size_t category = 0; category < 3; ++category) { if (!categories[category].spells.empty()) { 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 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); coord.top = spellCoord.top; ++i; } } } // 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); mSpellArea->setViewOffset(MyGUI::IntPoint(0, 0)); } } openmw-openmw-0.49.0/apps/openmw/mwgui/birth.hpp000066400000000000000000000026541503074453300216500ustar00rootroot00000000000000#ifndef MWGUI_BIRTH_H #define MWGUI_BIRTH_H #include "windowbase.hpp" #include namespace MWGui { class BirthDialog : public WindowModal { public: BirthDialog(); enum Gender { GM_Male, GM_Female }; const ESM::RefId& getBirthId() const { return mCurrentBirthId; } void setBirthId(const ESM::RefId& raceId); void setNextButtonShow(bool shown); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onSelectBirth(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateBirths(); void updateSpells(); MyGUI::ListBox* mBirthList; MyGUI::ScrollView* mSpellArea; MyGUI::ImageBox* mBirthImage; std::vector mSpellItems; ESM::RefId mCurrentBirthId; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/bookpage.cpp000066400000000000000000001337121503074453300223220ustar00rootroot00000000000000#include "bookpage.hpp" #include #include "MyGUI_FactoryManager.h" #include "MyGUI_FontManager.h" #include "MyGUI_RenderItem.h" #include "MyGUI_RenderManager.h" #include "MyGUI_TextureUtility.h" #include #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 BookTypesetter::Style { virtual ~Style() {} }; struct TypesetBookImpl : TypesetBook { typedef std::vector Content; typedef std::list Contents; typedef Utf8Stream::Point Utf8Point; typedef std::pair Range; struct StyleImpl : BookTypesetter::Style { 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 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); } 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); } }; typedef std::list Styles; struct Run { StyleImpl* mStyle; Range mRange; int mLeft, mRight; int mPrintableChars; }; typedef std::vector Runs; struct Line { Runs mRuns; MyGUI::IntRect mRect; }; typedef std::vector Lines; struct Section { Lines mLines; MyGUI::IntRect mRect; }; typedef std::vector

Sections; // 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 Pages; Pages mPages; Sections mSections; Contents mContents; Styles mStyles; MyGUI::IntRect mRect; virtual ~TypesetBookImpl() {} Range addContent(const BookTypesetter::Utf8Span& text) { Contents::iterator i = mContents.insert(mContents.end(), Content(text.first, text.second)); if (i->empty()) return Range(Utf8Point(nullptr), Utf8Point(nullptr)); return Range(i->data(), i->data() + i->size()); } size_t pageCount() const override { return mPages.size(); } std::pair getSize() const override { return std::make_pair(mRect.width(), mRect.height()); } 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 >= mRect.bottom || bottom <= i->mRect.top) continue; 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; 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; } StyleImpl* hitTest(int left, int top) const { for (Sections::const_iterator i = mSections.begin(); i != mSections.end(); ++i) { 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) { if (top < j->mRect.top || top >= j->mRect.bottom) continue; 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; } 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 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(); } virtual ~Typesetter() {} 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, 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; } Style* createHotStyle(Style* baseStyle, const Colour& normalColour, const Colour& hoverColour, const Colour& activeColour, InteractiveId id, bool unique) override { StyleImpl* BaseStyle = static_cast(baseStyle); 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; StyleImpl& style = *mBook->mStyles.insert(mBook->mStyles.end(), StyleImpl()); style.mFont = BaseStyle->mFont; style.mHotColour = hoverColour; style.mActiveColour = activeColour; style.mNormalColour = normalColour; style.mInteractiveId = id; return &style; } void write(Style* style, Utf8Span text) override { Range range = mBook->addContent(text); writeImpl(static_cast(style), range.first, range.second); } intptr_t addContent(Utf8Span text, bool select) override { add_partial_text(); Contents::iterator i = mBook->mContents.insert(mBook->mContents.end(), Content(text.first, text.second)); if (select) mCurrentContent = &(*i); return reinterpret_cast(&(*i)); } void selectContent(intptr_t contentHandle) override { add_partial_text(); mCurrentContent = reinterpret_cast(contentHandle); } void write(Style* style, size_t begin, size_t end) override { assert(mCurrentContent != nullptr); assert(end <= mCurrentContent->size()); assert(begin <= mCurrentContent->size()); Utf8Point begin_ = mCurrentContent->data() + begin; Utf8Point end_ = mCurrentContent->data() + end; writeImpl(static_cast(style), begin_, end_); } void lineBreak(float margin) override { assert(margin == 0); // TODO: figure out proper behavior here... add_partial_text(); mRun = nullptr; mLine = nullptr; } void sectionBreak(int margin) override { add_partial_text(); if (mBook->mSections.size() > 0) { mRun = nullptr; mLine = nullptr; mSection = nullptr; if (mBook->mRect.bottom < (mBook->mSections.back().mRect.bottom + margin)) mBook->mRect.bottom = (mBook->mSections.back().mRect.bottom + margin); } } void setSectionAlignment(Alignment sectionAlignment) override { add_partial_text(); if (mSection != nullptr) mSectionAlignment.back() = sectionAlignment; mCurrentAlignment = sectionAlignment; } TypesetBook::Ptr complete() override { int curPageStart = 0; int curPageStop = 0; add_partial_text(); 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; 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; } 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(); // 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) { // 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)); curPageStart = i->mRect.top; 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)); 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) { if (j->mRect.bottom > curPageStart + mPageHeight) { splitPos = j->mRect.top; break; } } mBook->mPages.push_back(Page(curPageStart, splitPos)); curPageStart = splitPos; curPageStop = splitPos; sectionHeightLeft = (i->mRect.bottom - splitPos); } curPageStop = i->mRect.bottom; } } if (curPageStart != curPageStop) mBook->mPages.push_back(Page(curPageStart, curPageStop)); return mBook; } void writeImpl(StyleImpl* style, Utf8Stream::Point _begin, Utf8Stream::Point _end) { Utf8Stream stream(_begin, _end); while (!stream.eof()) { if (ucsLineBreak(stream.peek())) { add_partial_text(); stream.consume(); mLine = nullptr; mRun = nullptr; continue; } if (ucsBreakingSpace(stream.peek()) && !mPartialWord.empty()) add_partial_text(); int word_width = 0; int space_width = 0; 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(); } 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(); } Utf8Stream::Point extent = stream.current(); 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); } } void add_partial_text() { if (mPartialWhitespace.empty() && mPartialWord.empty()) return; 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; 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) { int top = mLine ? mLine->mRect.top : mBook->mRect.bottom; append_run(i->mStyle, i->mBegin, i->mEnd, 0, left + i->mWidth, top + fontHeight); 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); left = mLine->mRect.right; } mPartialWhitespace.clear(); mPartialWord.clear(); } 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); } 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.bottom < bottom) mBook->mRect.bottom = bottom; if (mSection->mRect.right < right) mSection->mRect.right = right; if (mSection->mRect.bottom < bottom) mSection->mRect.bottom = bottom; if (mLine->mRect.right < right) mLine->mRect.right = right; 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; } } }; BookTypesetter::Ptr BookTypesetter::create(int pageWidth, int pageHeight) { return std::make_shared(pageWidth, pageHeight); } namespace { 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; } 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.top < clipTop) { tr.top += tr.height() * (clipTop - vr.top) / vr.height(); vr.top = clipTop; } if (vr.left < clipLeft) { tr.left += tr.width() * (clipLeft - vr.left) / vr.width(); vr.left = clipLeft; } if (vr.right > clipRight) { tr.right -= tr.width() * (vr.right - clipRight) / vr.width(); vr.right = clipRight; } if (vr.bottom > clipBottom) { tr.bottom -= tr.height() * (vr.bottom - clipBottom) / vr.height(); vr.bottom = clipBottom; } return true; } MyGUI::FloatPoint operator()(MyGUI::FloatPoint pt) { pt.left = absoluteLeft - leftOffset + pt.left; pt.top = absoluteTop - topOffset + pt.top; 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() = default; MyGUI::Vertex* end() const { return mVertices; } 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); mCursor.left = mOrigin.left + left; mCursor.top = mOrigin.top + top; } void emitGlyph(wchar_t ch) { MWGui::GlyphInfo info = GlyphInfo(mFont, ch); if (!info.charFound) return; MyGUI::FloatRect vr; vr.left = mCursor.left + info.bearingX; vr.top = mCursor.top + info.bearingY; vr.right = vr.left + info.width; vr.bottom = vr.top + info.height; MyGUI::FloatRect tr = info.uvRect; if (mRenderXform.clip(vr, tr)) quad(vr, tr); mCursor.left += static_cast(info.bearingX + info.advance); } void emitSpace(wchar_t ch) { MWGui::GlyphInfo info = GlyphInfo(mFont, ch); 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 vertex(float x, float y, float u, float v) { MyGUI::FloatPoint pt = mRenderXform(MyGUI::FloatPoint(x, y)); mVertices->x = pt.left; mVertices->y = pt.top; mVertices->z = mZ; mVertices->u = u; mVertices->v = v; mVertices->colour = mC; ++mVertices; } }; } class PageDisplay final : public MyGUI::ISubWidgetText { 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 { 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) { } void createDrawItem(MyGUI::ILayerNode* node) { assert(mRenderItem == nullptr); if (mTexture != nullptr) { mRenderItem = node->addToRenderItem(mTexture, false, false); mRenderItem->addDrawItem(this, mCountVertex); } } void destroyDrawItem(MyGUI::ILayerNode* node) { assert(mTexture != nullptr ? mRenderItem != nullptr : mRenderItem == nullptr); if (mTexture != nullptr) { mRenderItem->removeDrawItem(this); mRenderItem = nullptr; } } 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 {} }; void resetPage() { mIsPageReset = true; mPage = 0; } void setPage(size_t page) { mIsPageReset = false; 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); pos.left -= mCroppedParent->getAbsoluteLeft(); pos.top -= mCroppedParent->getAbsoluteTop(); pos.top += mViewTop; return pos; } public: typedef TypesetBookImpl::StyleImpl Style; typedef std::map> ActiveTextFormats; int mViewTop; int mViewBottom; Style* mFocusItem; bool mItemActive; MyGUI::MouseButton mLastDown; std::function mLinkClicked; std::shared_ptr mBook; MyGUI::ILayerNode* mNode; ActiveTextFormats mActiveTextFormats; PageDisplay() { resetPage(); mViewTop = 0; mViewBottom = 0; mFocusItem = nullptr; mItemActive = false; mNode = nullptr; } void dirtyFocusItem() { if (mFocusItem != nullptr) { MyGUI::IFont* Font = mBook->affectedFont(mFocusItem); ActiveTextFormats::iterator i = mActiveTextFormats.find(Font); if (mNode && i != mActiveTextFormats.end()) mNode->outOfDate(i->second->mRenderItem); } } void onMouseLostFocus() { if (!mBook) return; if (mPage >= mBook->mPages.size()) return; dirtyFocusItem(); mFocusItem = nullptr; mItemActive = false; } 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); if (mLastDown == MyGUI::MouseButton::None) { if (hit != mFocusItem) { dirtyFocusItem(); mFocusItem = hit; mItemActive = false; dirtyFocusItem(); } } else if (mFocusItem != nullptr) { bool newItemActive = hit == mFocusItem; if (newItemActive != mItemActive) { mItemActive = newItemActive; dirtyFocusItem(); } } } void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id) { auto pos = getAdjustedPos(left, top); if (pos && mLastDown == MyGUI::MouseButton::None) { mFocusItem = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr; mItemActive = true; dirtyFocusItem(); mLastDown = id; } } void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) { auto pos = getAdjustedPos(left, top); if (pos && mLastDown == id) { Style* item = pos->top <= mViewBottom ? mBook->hitTestWithMargin(pos->left, pos->top) : nullptr; bool clicked = mFocusItem == item; mItemActive = false; dirtyFocusItem(); mLastDown = MyGUI::MouseButton::None; if (clicked && mLinkClicked && item && item->mInteractiveId != 0) mLinkClicked(item->mInteractiveId); } } void showPage(TypesetBook::Ptr book, size_t newPage) { std::shared_ptr newBook = std::dynamic_pointer_cast(book); if (mBook != newBook) { mFocusItem = nullptr; mItemActive = 0; for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) { if (mNode != nullptr && i->second != nullptr) i->second->destroyDrawItem(mNode); i->second.reset(); } mActiveTextFormats.clear(); if (newBook != nullptr) { 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 if (mBook && isPageDifferent(newPage)) { if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) mNode->outOfDate(i->second->mRenderItem); setPage(newPage); 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 { MyGUI::IFont* Font = run.mStyle->mFont; ActiveTextFormats::iterator j = this_->mActiveTextFormats.find(Font); if (j == this_->mActiveTextFormats.end()) { auto textFormat = std::make_unique(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 createActiveFormats(std::shared_ptr newBook) { newBook->visitRuns(0, 0x7FFFFFFF, CreateActiveFormat(this)); if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) i->second->createDrawItem(mNode); } void setVisible(bool newVisible) override { if (mVisible == newVisible) return; mVisible = newVisible; if (mVisible) { // reset input state mLastDown = MyGUI::MouseButton::None; mFocusItem = nullptr; mItemActive = 0; } if (nullptr != mNode) { for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) mNode->outOfDate(i->second->mRenderItem); } } 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); } struct RenderRun { PageDisplay* this_; GlyphStream& glyphStream; RenderRun(PageDisplay* this_, GlyphStream& glyphStream) : this_(this_) , glyphStream(glyphStream) { } void operator()(Section const& section, Line const& line, Run const& run) const { bool isActive = run.mStyle->mInteractiveId && (run.mStyle == this_->mFocusItem); MyGUI::Colour colour = isActive ? (this_->mItemActive ? run.mStyle->mActiveColour : run.mStyle->mHotColour) : run.mStyle->mNormalColour; glyphStream.reset(static_cast(section.mRect.left + line.mRect.left + run.mLeft), static_cast(line.mRect.top), colour); Utf8Stream stream(run.mRange); while (!stream.eof()) { Utf8Stream::UnicodeChar code_point = stream.consume(); if (ucsCarriageReturn(code_point)) continue; if (!ucsSpace(code_point)) glyphStream.emitGlyph(code_point); else glyphStream.emitSpace(code_point); } } }; /* queue up rendering operations for this text format */ void doRender(TextFormat& textFormat) { if (!mVisible) return; MyGUI::Vertex* vertices = textFormat.mRenderItem->getCurrentVertexBuffer(); RenderXform renderXform(mCroppedParent, textFormat.mRenderItem->getRenderTarget()->getInfo()); float z = SceneUtil::AutoDepth::isReversed() ? 1.f : -1.f; GlyphStream glyphStream(textFormat.mFont, static_cast(mCoord.left), static_cast(mCoord.top - mViewTop), z /*mNode->getNodeDepth()*/, vertices, renderXform); int visit_top = (std::max)(mViewTop, mViewTop + int(renderXform.clipTop)); int visit_bottom = (std::min)(mViewBottom, mViewTop + int(renderXform.clipBottom)); mBook->visitRuns(visit_top, visit_bottom, textFormat.mFont, RenderRun(this, glyphStream)); textFormat.mRenderItem->setLastVertexCount(glyphStream.end() - vertices); } // ISubWidget should not necessarily be a drawitem // in this case, it is not... void doRender() override {} void _updateView() override { _checkMargin(); if (mNode != nullptr) for (ActiveTextFormats::iterator i = mActiveTextFormats.begin(); i != mActiveTextFormats.end(); ++i) mNode->outOfDate(i->second->mRenderItem); } void _correctView() 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); mNode = 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(std::move(book), page); } void adviseLinkClicked(std::function linkClicked) override { mPageDisplay->mLinkClicked = std::move(linkClicked); } void unadviseLinkClicked() override { mPageDisplay->mLinkClicked = std::function(); } protected: void initialiseOverride() override { Base::initialiseOverride(); if (getSubWidgetText()) { mPageDisplay = getSubWidgetText()->castType(); } else { throw std::runtime_error("BookPage unable to find page display sub widget"); } } 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 onMouseMove(int left, int top) override { mPageDisplay->onMouseMove(left, top); } void onMouseButtonPressed(int left, int top, MyGUI::MouseButton id) override { mPageDisplay->onMouseButtonPressed(left, top, id); } void onMouseButtonReleased(int left, int top, MyGUI::MouseButton id) override { mPageDisplay->onMouseButtonReleased(left, top, id); } 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'; } // Normal no-break space (0x00A0) is ignored here // because Morrowind compatibility requires us to render its glyph static bool ucsSpace(int codePoint) { switch (codePoint) { case 0x0020: // 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; } } // No-break spaces (0x00A0, 0x202F, 0xFEFF - normal, narrow, zero width) // are ignored here for obvious reasons // Figure space (0x2007) is not a breaking space either static bool ucsBreakingSpace(int codePoint) { switch (codePoint) { case 0x0020: // 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 0x2008: // PUNCTUATION SPACE case 0x2009: // THIN SPACE case 0x200A: // HAIR SPACE case 0x200B: // ZERO WIDTH SPACE case 0x205F: // MEDIUM MATHEMATICAL SPACE case 0x3000: // IDEOGRAPHIC SPACE return true; default: return false; } } } openmw-openmw-0.49.0/apps/openmw/mwgui/bookpage.hpp000066400000000000000000000142531503074453300223250ustar00rootroot00000000000000#ifndef MWGUI_BOOKPAGE_HPP #define MWGUI_BOOKPAGE_HPP #include "MyGUI_Colour.h" #include "MyGUI_IFont.h" #include "MyGUI_Widget.h" #include #include #include #include namespace MWGui { /// A formatted and paginated document to be used with /// the book page widget. struct TypesetBook { typedef std::shared_ptr Ptr; typedef intptr_t InteractiveId; /// Returns the number of pages in the document. 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 /// the largest coordinate of the right edge of any line, /// 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 ~TypesetBook() = default; }; struct GlyphInfo { char codePoint; float width; float height; float advance; float bearingX; float bearingY; bool charFound; MyGUI::FloatRect uvRect; GlyphInfo(MyGUI::IFont* font, MyGUI::Char ch) { const MyGUI::GlyphInfo* gi = font->getGlyphInfo(ch); if (gi) { 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; uvRect = gi->uvRect; charFound = true; } else { codePoint = 0; bearingX = 0; bearingY = 0; width = 0; height = 0; advance = 0; charFound = false; } } }; /// A factory class for creating a typeset book instance. struct BookTypesetter { typedef std::shared_ptr Ptr; typedef TypesetBook::InteractiveId InteractiveId; typedef MyGUI::Colour Colour; typedef uint8_t const* Utf8Point; typedef std::pair Utf8Span; virtual ~BookTypesetter() = default; enum Alignment { AlignLeft = -1, AlignCenter = 0, AlignRight = +1 }; /// Styles are used to control the character level formatting /// of text added to a typeset book. Their lifetime is equal /// to the lifetime of the book-typesetter instance that created /// them. struct Style; /// A factory function for creating the default implementation of a book typesetter 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; /// 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; /// 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; /// 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; /// Changes the alignment for the current section of text. 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; /// 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; /// Select a previously created content block for future writes. 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; /// Finalize the document layout, and return a pointer to it. virtual TypesetBook::Ptr complete() = 0; }; /// An interface to the BookPage widget. class BookPage : public MyGUI::Widget { MYGUI_RTTI_DERIVED(BookPage) public: typedef TypesetBook::InteractiveId InteractiveId; 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; /// Set the callback for a clicking a hyper-link in the document. virtual void adviseLinkClicked(ClickCallback callback) = 0; /// Clear the hyper-link click callback. 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(); }; } #endif // MWGUI_BOOKPAGE_HPP openmw-openmw-0.49.0/apps/openmw/mwgui/bookwindow.cpp000066400000000000000000000164331503074453300227150ustar00rootroot00000000000000#include "bookwindow.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/class.hpp" #include "formatting.hpp" namespace MWGui { BookWindow::BookWindow() : BookWindowBase("openmw_book.layout") , mCurrentPage(0) , mTakeButtonShow(true) , mTakeButtonAllowed(true) { getWidget(mCloseButton, "CloseButton"); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onCloseButtonClicked); getWidget(mTakeButton, "TakeButton"); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onTakeButtonClicked); getWidget(mNextPageButton, "NextPageBTN"); mNextPageButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onNextPageButtonClicked); getWidget(mPrevPageButton, "PrevPageBTN"); mPrevPageButton->eventMouseButtonClick += MyGUI::newDelegate(this, &BookWindow::onPrevPageButtonClicked); getWidget(mLeftPageNumber, "LeftPageNumber"); getWidget(mRightPageNumber, "RightPageNumber"); getWidget(mLeftPage, "LeftPage"); getWidget(mRightPage, "RightPage"); adjustButton("CloseButton"); adjustButton("TakeButton"); adjustButton("PrevPageBTN"); float scale = adjustButton("NextPageBTN"); mLeftPage->setNeedMouseFocus(true); mLeftPage->eventMouseWheel += MyGUI::newDelegate(this, &BookWindow::onMouseWheel); mRightPage->setNeedMouseFocus(true); mRightPage->eventMouseWheel += MyGUI::newDelegate(this, &BookWindow::onMouseWheel); mNextPageButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mPrevPageButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mTakeButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); mCloseButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &BookWindow::onKeyButtonPressed); 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)); } center(); } void BookWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (_rel < 0) nextPage(); else prevPage(); } void BookWindow::clearPages() { mPages.clear(); } 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(); bool showTakeButton = book.getContainerStore() != &player.getClass().getContainerStore(player); clearPages(); mCurrentPage = 0; 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, *text, shrinkTextAtLastTag); formatter.markupToWidget(mRightPage, *text, shrinkTextAtLastTag); updatePages(); setTakeButtonShow(showTakeButton); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); } void BookWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void BookWindow::onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) prevPage(); else if (key == MyGUI::KeyCode::ArrowDown) nextPage(); } void BookWindow::setInventoryAllowed(bool allowed) { mTakeButtonAllowed = allowed; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void BookWindow::onCloseButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } void BookWindow::onTakeButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Book Up")); MWWorld::ActionTake take(mBook); take.execute(MWMechanics::getPlayer()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Book); } void BookWindow::onNextPageButtonClicked(MyGUI::Widget* sender) { nextPage(); } 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)); MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool nextPageVisible = (mCurrentPage + 1) * 2 < mPages.size(); mNextPageButton->setVisible(nextPageVisible); bool prevPageVisible = mCurrentPage != 0; mPrevPageButton->setVisible(prevPageVisible); if (focus == mNextPageButton && !nextPageVisible && prevPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mPrevPageButton); else if (focus == mPrevPageButton && !prevPageVisible && nextPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNextPageButton); if (mPages.empty()) return; MyGUI::Widget* paper; paper = mLeftPage->getChildAt(0); 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()) { paper->setCoord(paper->getPosition().left, -mPages[mCurrentPage * 2 + 1].first, paper->getWidth(), mPages[mCurrentPage * 2 + 1].second); paper->setVisible(true); } else { paper->setVisible(false); } } void BookWindow::nextPage() { if ((mCurrentPage + 1) * 2 < mPages.size()) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page2")); ++mCurrentPage; updatePages(); } } void BookWindow::prevPage() { if (mCurrentPage > 0) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); --mCurrentPage; updatePages(); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/bookwindow.hpp000066400000000000000000000032131503074453300227120ustar00rootroot00000000000000#ifndef MWGUI_BOOKWINDOW_H #define MWGUI_BOOKWINDOW_H #include "windowbase.hpp" #include "../mwworld/ptr.hpp" #include namespace MWGui { class BookWindow : public BookWindowBase { public: BookWindow(); void setPtr(const MWWorld::Ptr& book) override; void setInventoryAllowed(bool allowed); void onResChange(int, int) override { center(); } std::string_view getWindowIdForLua() const override { return "Book"; } 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 onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); void nextPage(); void prevPage(); void updatePages(); void clearPages(); private: typedef std::pair Page; typedef std::vector Pages; Gui::ImageButton* mCloseButton; Gui::ImageButton* mTakeButton; Gui::ImageButton* mNextPageButton; Gui::ImageButton* mPrevPageButton; MyGUI::TextBox* mLeftPageNumber; MyGUI::TextBox* mRightPageNumber; MyGUI::Widget* mLeftPage; MyGUI::Widget* mRightPage; unsigned int mCurrentPage; // 0 is first page Pages mPages; MWWorld::Ptr mBook; bool mTakeButtonShow; bool mTakeButtonAllowed; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/charactercreation.cpp000066400000000000000000000710001503074453300242030ustar00rootroot00000000000000#include "charactercreation.hpp" #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/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "birth.hpp" #include "class.hpp" #include "inventorywindow.hpp" #include "race.hpp" #include "review.hpp" #include "textinput.hpp" namespace { struct Response { const std::string mText; const ESM::Class::Specialization mSpecialization; }; struct Step { const std::string mText; const Response mResponses[3]; 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 sound = "vo\\misc\\chargen qa" + MyGUI::utility::toString(number) + ".wav"; 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); switch (order) { case 0: return { std::move(question), { std::move(r0), std::move(r1), std::move(r2) }, std::move(sound) }; case 1: return { std::move(question), { std::move(r0), std::move(r2), std::move(r1) }, std::move(sound) }; case 2: return { std::move(question), { std::move(r1), std::move(r0), std::move(r2) }, std::move(sound) }; case 3: return { std::move(question), { std::move(r1), std::move(r2), std::move(r0) }, std::move(sound) }; case 4: return { std::move(question), { std::move(r2), std::move(r0), std::move(r1) }, std::move(sound) }; default: return { std::move(question), { std::move(r2), std::move(r1), std::move(r0) }, std::move(sound) }; } } } namespace MWGui { CharacterCreation::CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem) : mParent(parent) , mResourceSystem(resourceSystem) , mGenerateClassStep(0) { mCreationStage = CSE_NotStarted; mGenerateClassResponses[0] = ESM::Class::Combat; mGenerateClassResponses[1] = ESM::Class::Magic; mGenerateClassResponses[2] = ESM::Class::Stealth; mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; // Setup player stats const auto& store = MWBase::Environment::get().getWorld()->getStore(); for (const ESM::Attribute& attribute : store.get()) mPlayerAttributes.emplace(attribute.mId, MWMechanics::AttributeValue()); for (const auto& skill : store.get()) mPlayerSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); } void CharacterCreation::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { mPlayerAttributes[id] = value; if (mReviewDialog) mReviewDialog->setAttribute(id, value); } void CharacterCreation::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { if (mReviewDialog) { if (id == "HBar") { mReviewDialog->setHealth(value); } else if (id == "MBar") { mReviewDialog->setMagicka(value); } else if (id == "FBar") { mReviewDialog->setFatigue(value); } } } void CharacterCreation::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { mPlayerSkillValues[id] = value; if (mReviewDialog) mReviewDialog->setSkillValue(id, value); } void CharacterCreation::configureSkills(const std::vector& major, const std::vector& minor) { if (mReviewDialog) mReviewDialog->configureSkills(major, minor); mPlayerMajorSkills = major; mPlayerMinorSkills = minor; } void CharacterCreation::onFrame(float duration) { if (mReviewDialog) mReviewDialog->onFrame(duration); } void CharacterCreation::spawnDialog(const char id) { try { switch (id) { case GM_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); mNameDialog->setVisible(true); break; case GM_Race: 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); mRaceDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onRaceDialogBack); mRaceDialog->setVisible(true); if (mCreationStage < CSE_NameChosen) mCreationStage = CSE_NameChosen; break; case GM_Class: 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(std::move(mPickClassDialog)); mPickClassDialog = std::make_unique(); mPickClassDialog->setNextButtonShow(mCreationStage >= CSE_ClassChosen); mPickClassDialog->setClassId(mPlayerClass.mId); mPickClassDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onPickClassDialogDone); mPickClassDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onPickClassDialogBack); mPickClassDialog->setVisible(true); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_Birth: 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); mBirthSignDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onBirthSignDialogBack); mBirthSignDialog->setVisible(true); if (mCreationStage < CSE_ClassChosen) mCreationStage = CSE_ClassChosen; break; case GM_ClassCreate: if (mCreateClassDialog == nullptr) { 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); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_ClassGenerate: mGenerateClassStep = 0; mGenerateClass = ESM::RefId(); mGenerateClassSpecializations[0] = 0; mGenerateClassSpecializations[1] = 0; mGenerateClassSpecializations[2] = 0; showClassQuestionDialog(); if (mCreationStage < CSE_RaceChosen) mCreationStage = CSE_RaceChosen; break; case GM_Review: MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); mReviewDialog = std::make_unique(); MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::NPC* playerNpc = world->getPlayerPtr().get()->mBase; const MWWorld::Player& player = world->getPlayer(); const ESM::Class* playerClass = world->getStore().get().find(playerNpc->mClass); mReviewDialog->setPlayerName(playerNpc->mName); mReviewDialog->setRace(playerNpc->mRace); mReviewDialog->setClass(*playerClass); mReviewDialog->setBirthSign(player.getBirthSign()); MWWorld::Ptr playerPtr = MWMechanics::getPlayer(); const MWMechanics::CreatureStats& stats = playerPtr.getClass().getCreatureStats(playerPtr); mReviewDialog->setHealth(stats.getHealth()); mReviewDialog->setMagicka(stats.getMagicka()); mReviewDialog->setFatigue(stats.getFatigue()); for (auto& attributePair : mPlayerAttributes) { mReviewDialog->setAttribute(attributePair.first, attributePair.second); } for (const auto& [skill, value] : mPlayerSkillValues) { 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->setVisible(true); if (mCreationStage < CSE_BirthSignChosen) mCreationStage = CSE_BirthSignChosen; break; } } catch (std::exception& e) { Log(Debug::Error) << "Error: Failed to create chargen window: " << e.what(); } } void CharacterCreation::onReviewDialogDone(WindowBase* parWindow) { MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); MWBase::Environment::get().getWindowManager()->popGuiMode(); } void CharacterCreation::onReviewDialogBack() { MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); mCreationStage = CSE_ReviewBack; MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Birth); } void CharacterCreation::onReviewActivateDialog(int parDialog) { MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mReviewDialog)); mCreationStage = CSE_ReviewNext; MWBase::Environment::get().getWindowManager()->popGuiMode(); switch (parDialog) { case ReviewDialog::NAME_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Name); break; case ReviewDialog::RACE_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Race); break; case ReviewDialog::CLASS_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); break; case ReviewDialog::BIRTHSIGN_DIALOG: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Birth); }; } void CharacterCreation::selectPickedClass() { if (mPickClassDialog) { const ESM::RefId& classId = mPickClassDialog->getClassId(); if (!classId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerClass(classId); const ESM::Class* pickedClass = MWBase::Environment::get().getESMStore()->get().find(classId); if (pickedClass) { mPlayerClass = *pickedClass; } MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mPickClassDialog)); } } void CharacterCreation::onPickClassDialogDone(WindowBase* parWindow) { selectPickedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); } void CharacterCreation::onPickClassDialogBack() { selectPickedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::onClassChoice(int _index) { MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mClassChoiceDialog)); MWBase::Environment::get().getWindowManager()->popGuiMode(); switch (_index) { case ClassChoiceDialog::Class_Generate: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassGenerate); break; case ClassChoiceDialog::Class_Pick: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassPick); break; case ClassChoiceDialog::Class_Create: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_ClassCreate); break; case ClassChoiceDialog::Class_Back: MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Race); break; }; } void CharacterCreation::onNameDialogDone(WindowBase* parWindow) { if (mNameDialog) { mPlayerName = mNameDialog->getTextInput(); MWBase::Environment::get().getMechanicsManager()->setPlayerName(mPlayerName); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mNameDialog)); } handleDialogDone(CSE_NameChosen, GM_Race); } void CharacterCreation::selectRace() { if (mRaceDialog) { const ESM::NPC& data = mRaceDialog->getResult(); mPlayerRaceId = data.mRace; if (!mPlayerRaceId.empty()) { MWBase::Environment::get().getMechanicsManager()->setPlayerRace( data.mRace, data.isMale(), data.mHead, data.mHair); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->rebuildAvatar(); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mRaceDialog)); } } void CharacterCreation::onRaceDialogBack() { selectRace(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Name); } void CharacterCreation::onRaceDialogDone(WindowBase* parWindow) { selectRace(); handleDialogDone(CSE_RaceChosen, GM_Class); } void CharacterCreation::selectBirthSign() { if (mBirthSignDialog) { mPlayerBirthSignId = mBirthSignDialog->getBirthId(); if (!mPlayerBirthSignId.empty()) MWBase::Environment::get().getMechanicsManager()->setPlayerBirthsign(mPlayerBirthSignId); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mBirthSignDialog)); } } void CharacterCreation::onBirthSignDialogDone(WindowBase* parWindow) { selectBirthSign(); handleDialogDone(CSE_BirthSignChosen, GM_Review); } void CharacterCreation::onBirthSignDialogBack() { selectBirthSign(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::selectCreatedClass() { if (mCreateClassDialog) { 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) { 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(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); } } void CharacterCreation::onCreateClassDialogDone(WindowBase* parWindow) { selectCreatedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); } void CharacterCreation::onCreateClassDialogBack() { // not done in MW, but we do it for consistency with the other dialogs selectCreatedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::onClassQuestionChosen(int _index) { MWBase::Environment::get().getSoundManager()->stopSay(); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassQuestionDialog)); if (_index < 0 || _index >= 3) { MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); return; } ESM::Class::Specialization specialization = mGenerateClassResponses[_index]; if (specialization == ESM::Class::Combat) ++mGenerateClassSpecializations[0]; else if (specialization == ESM::Class::Magic) ++mGenerateClassSpecializations[1]; else if (specialization == ESM::Class::Stealth) ++mGenerateClassSpecializations[2]; ++mGenerateClassStep; showClassQuestionDialog(); } void CharacterCreation::showClassQuestionDialog() { if (mGenerateClassStep == 10) { unsigned combat = mGenerateClassSpecializations[0]; unsigned magic = mGenerateClassSpecializations[1]; unsigned stealth = mGenerateClassSpecializations[2]; std::string_view className; if (combat > 7) { className = "Warrior"; } else if (magic > 7) { className = "Mage"; } else if (stealth > 7) { className = "Thief"; } else { switch (combat) { case 4: className = "Rogue"; break; case 5: if (stealth == 3) className = "Scout"; else className = "Archer"; break; case 6: if (stealth == 1) className = "Barbarian"; else if (stealth == 3) className = "Crusader"; else className = "Knight"; break; case 7: className = "Warrior"; break; default: switch (magic) { case 4: className = "Spellsword"; break; case 5: className = "Witchhunter"; break; case 6: if (combat == 2) className = "Sorcerer"; else if (combat == 3) className = "Healer"; else className = "Battlemage"; break; case 7: className = "Mage"; break; default: switch (stealth) { case 3: if (magic == 3) className = "Bard"; // unreachable else className = "Warrior"; break; case 5: if (magic == 3) className = "Monk"; else className = "Pilgrim"; break; case 6: if (magic == 1) className = "Agent"; else if (magic == 3) className = "Assassin"; else className = "Acrobat"; break; case 7: className = "Thief"; break; default: className = "Warrior"; } } } } mGenerateClass = ESM::RefId::stringRefId(className); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassResultDialog)); mGenerateClassResultDialog = std::make_unique(); mGenerateClassResultDialog->setClassId(mGenerateClass); mGenerateClassResultDialog->eventBack += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassBack); mGenerateClassResultDialog->eventDone += MyGUI::newDelegate(this, &CharacterCreation::onGenerateClassDone); mGenerateClassResultDialog->setVisible(true); return; } if (mGenerateClassStep > 10) { MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); return; } MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassQuestionDialog)); mGenerateClassQuestionDialog = std::make_unique(); Step step = sGenerateClassSteps(mGenerateClassStep); mGenerateClassResponses[0] = step.mResponses[0].mSpecialization; mGenerateClassResponses[1] = step.mResponses[1].mSpecialization; mGenerateClassResponses[2] = step.mResponses[2].mSpecialization; InfoBoxDialog::ButtonList buttons; mGenerateClassQuestionDialog->setText(step.mText); buttons.push_back(step.mResponses[0].mText); 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->setVisible(true); MWBase::Environment::get().getSoundManager()->say(Misc::ResourceHelpers::correctSoundPath(step.mSound)); } void CharacterCreation::selectGeneratedClass() { MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mGenerateClassResultDialog)); MWBase::Environment::get().getMechanicsManager()->setPlayerClass(mGenerateClass); const ESM::Class* generatedClass = MWBase::Environment::get().getESMStore()->get().find(mGenerateClass); mPlayerClass = *generatedClass; } void CharacterCreation::onGenerateClassBack() { selectGeneratedClass(); MWBase::Environment::get().getWindowManager()->popGuiMode(); MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Class); } void CharacterCreation::onGenerateClassDone(WindowBase* parWindow) { selectGeneratedClass(); handleDialogDone(CSE_ClassChosen, GM_Birth); } CharacterCreation::~CharacterCreation() = default; void CharacterCreation::handleDialogDone(CSE currentStage, int nextMode) { MWBase::Environment::get().getWindowManager()->popGuiMode(); if (mCreationStage == CSE_ReviewNext) { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Review); } else if (mCreationStage >= currentStage) { MWBase::Environment::get().getWindowManager()->pushGuiMode((GuiMode)nextMode); } else { mCreationStage = currentStage; } } } openmw-openmw-0.49.0/apps/openmw/mwgui/charactercreation.hpp000066400000000000000000000104161503074453300242140ustar00rootroot00000000000000#ifndef CHARACTER_CREATION_HPP #define CHARACTER_CREATION_HPP #include #include #include #include #include "statswatcher.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { class WindowBase; class TextInputDialog; class InfoBoxDialog; class RaceDialog; class DialogueWindow; class ClassChoiceDialog; class GenerateClassResultDialog; class PickClassDialog; class CreateClassDialog; class BirthDialog; class ReviewDialog; class MessageBoxManager; class CharacterCreation : public StatsListener { public: CharacterCreation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); virtual ~CharacterCreation(); // 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 onFrame(float duration); private: 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/class.cpp000066400000000000000000000767641503074453300216550ustar00rootroot00000000000000#include "class.hpp" #include #include #include #include #include #include "../mwbase/environment.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) { return left.second.compare(right.second) < 0; } } namespace MWGui { /* GenerateClassResultDialog */ GenerateClassResultDialog::GenerateClassResultDialog() : WindowModal("openmw_chargen_generate_class_result.layout") { setText("ReflectT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sMessageQuestionAnswer1", {})); getWidget(mClassImage, "ClassImage"); getWidget(mClassName, "ClassName"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->setCaptionWithReplacing("#{sMessageQuestionAnswer3}"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaptionWithReplacing("#{sMessageQuestionAnswer2}"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &GenerateClassResultDialog::onOkClicked); center(); } void GenerateClassResultDialog::setClassId(const ESM::RefId& classId) { mCurrentClassId = classId; setClassImage(mClassImage, mCurrentClassId); mClassName->setCaption( MWBase::Environment::get().getESMStore()->get().find(mCurrentClassId)->mName); center(); } // widget controls void GenerateClassResultDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void GenerateClassResultDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } /* PickClassDialog */ PickClassDialog::PickClassDialog() : WindowModal("openmw_chargen_class.layout") { // Centre dialog center(); getWidget(mSpecializationName, "SpecializationName"); getWidget(mFavoriteAttribute[0], "FavoriteAttribute0"); getWidget(mFavoriteAttribute[1], "FavoriteAttribute1"); for (int i = 0; i < 5; i++) { char theIndex = '0' + i; getWidget(mMajorSkill[i], std::string("MajorSkill").append(1, theIndex)); getWidget(mMinorSkill[i], std::string("MinorSkill").append(1, theIndex)); } getWidget(mClassList, "ClassList"); mClassList->setScrollVisible(true); mClassList->eventListSelectAccept += MyGUI::newDelegate(this, &PickClassDialog::onAccept); mClassList->eventListChangePosition += MyGUI::newDelegate(this, &PickClassDialog::onSelectClass); getWidget(mClassImage, "ClassImage"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PickClassDialog::onOkClicked); updateClasses(); updateStats(); } void PickClassDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void PickClassDialog::onOpen() { WindowModal::onOpen(); updateClasses(); updateStats(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mClassList); // Show the current class by default MWWorld::Ptr player = MWMechanics::getPlayer(); const ESM::RefId& classId = player.get()->mBase->mClass; if (!classId.empty()) setClassId(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 (*mClassList->getItemDataAt(i) == classId) { mClassList->setIndexSelected(i); break; } } updateStats(); } // widget controls void PickClassDialog::onOkClicked(MyGUI::Widget* _sender) { if (mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void PickClassDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void PickClassDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectClass(_sender, _index); if (mClassList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void PickClassDialog::onSelectClass(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; const ESM::RefId& classId = *mClassList->getItemDataAt(_index); if (mCurrentClassId == classId) return; mCurrentClassId = classId; updateStats(); } // update widget content void PickClassDialog::updateClasses() { mClassList->removeAllItems(); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::vector> items; // class id, class name for (const ESM::Class& classInfo : store.get()) { bool playable = (classInfo.mData.mIsPlayable != 0); if (!playable) // Only display playable classes continue; if (store.get().isDynamic(classInfo.mId)) continue; // custom-made class not relevant for this dialog items.emplace_back(classInfo.mId, classInfo.mName); } std::sort(items.begin(), items.end(), sortClasses); int index = 0; for (auto& itemPair : items) { const ESM::RefId& id = itemPair.first; mClassList->addItem(itemPair.second, id); if (mCurrentClassId.empty()) { mCurrentClassId = id; mClassList->setIndexSelected(index); } else if (id == mCurrentClassId) { mClassList->setIndexSelected(index); } ++index; } } void PickClassDialog::updateStats() { if (mCurrentClassId.empty()) return; 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(currentClass->mData.mSpecialization); 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(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 (size_t i = 0; i < currentClass->mData.mSkills.size(); ++i) { 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); } /* InfoBoxDialog */ void InfoBoxDialog::fitToText(MyGUI::TextBox* widget) { MyGUI::IntCoord inner = widget->getTextRegion(); MyGUI::IntCoord outer = widget->getCoord(); MyGUI::IntSize size = widget->getTextSize(); size.width += outer.width - inner.width; size.height += outer.height - inner.height; widget->setSize(size); } void InfoBoxDialog::layoutVertically(MyGUI::Widget* widget, int margin) { size_t count = widget->getChildCount(); int pos = 0; pos += margin; int width = 0; for (unsigned i = 0; i < count; ++i) { MyGUI::Widget* child = widget->getChildAt(i); if (!child->getVisible()) continue; child->setPosition(child->getLeft(), pos); width = std::max(width, child->getWidth()); pos += child->getHeight() + margin; } width += margin * 2; widget->setSize(width, pos); } InfoBoxDialog::InfoBoxDialog() : WindowModal("openmw_infobox.layout") { getWidget(mTextBox, "TextBox"); getWidget(mText, "Text"); mText->getSubWidgetText()->setWordWrap(true); getWidget(mButtonBar, "ButtonBar"); center(); } void InfoBoxDialog::setText(const std::string& str) { mText->setCaption(str); mTextBox->setVisible(!str.empty()); fitToText(mText); } std::string InfoBoxDialog::getText() const { return mText->getCaption(); } void InfoBoxDialog::setButtons(ButtonList& buttons) { for (MyGUI::Button* button : this->mButtons) { MyGUI::Gui::getInstance().destroyWidget(button); } this->mButtons.clear(); // 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) { button = mButtonBar->createWidget( "MW_Button", coord, MyGUI::Align::Top | MyGUI::Align::HCenter, {}); button->getSubWidgetText()->setWordWrap(true); button->setCaption(text); fitToText(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InfoBoxDialog::onButtonClicked); coord.top += button->getHeight(); this->mButtons.push_back(button); } } void InfoBoxDialog::onOpen() { WindowModal::onOpen(); // Fix layout layoutVertically(mTextBox, 4); layoutVertically(mButtonBar, 6); layoutVertically(mMainWidget, 4 + 6); center(); } void InfoBoxDialog::onButtonClicked(MyGUI::Widget* _sender) { int i = 0; for (MyGUI::Button* button : mButtons) { if (button == _sender) { eventButtonSelected(i); return; } ++i; } } /* ClassChoiceDialog */ ClassChoiceDialog::ClassChoiceDialog() : InfoBoxDialog() { setText({}); ButtonList buttons; 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") , mAffectedAttribute(nullptr) , mAffectedSkill(nullptr) { // Centre dialog center(); setText("SpecializationT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sChooseClassMenu1", "Specialization")); getWidget(mSpecializationName, "SpecializationName"); mSpecializationName->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationClicked); 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++) { 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]); mSkills.push_back(mMinorSkill[i]); } for (Widgets::MWSkillPtr& skill : mSkills) { skill->eventClicked += MyGUI::newDelegate(this, &CreateClassDialog::onSkillClicked); } setText("LabelT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sName", {})); getWidget(mEditName, "EditName"); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mEditName); MyGUI::Button* descriptionButton; getWidget(descriptionButton, "DescriptionButton"); descriptionButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionClicked); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CreateClassDialog::onOkClicked); // Set default skills, attributes mFavoriteAttribute0->setAttributeId(ESM::Attribute::Strength); mFavoriteAttribute1->setAttributeId(ESM::Attribute::Agility); mMajorSkill[0]->setSkillId(ESM::Skill::Block); mMajorSkill[1]->setSkillId(ESM::Skill::Armorer); mMajorSkill[2]->setSkillId(ESM::Skill::MediumArmor); mMajorSkill[3]->setSkillId(ESM::Skill::HeavyArmor); mMajorSkill[4]->setSkillId(ESM::Skill::BluntWeapon); mMinorSkill[0]->setSkillId(ESM::Skill::LongBlade); mMinorSkill[1]->setSkillId(ESM::Skill::Axe); mMinorSkill[2]->setSkillId(ESM::Skill::Spear); mMinorSkill[3]->setSkillId(ESM::Skill::Athletics); mMinorSkill[4]->setSkillId(ESM::Skill::Enchant); setSpecialization(0); update(); } CreateClassDialog::~CreateClassDialog() = default; void CreateClassDialog::update() { for (int i = 0; i < 5; ++i) { ToolTips::createSkillToolTip(mMajorSkill[i], mMajorSkill[i]->getSkillId()); ToolTips::createSkillToolTip(mMinorSkill[i], mMinorSkill[i]->getSkillId()); } ToolTips::createAttributeToolTip(mFavoriteAttribute0, mFavoriteAttribute0->getAttributeId()); ToolTips::createAttributeToolTip(mFavoriteAttribute1, mFavoriteAttribute1->getAttributeId()); } std::string CreateClassDialog::getName() const { return mEditName->getCaption(); } std::string CreateClassDialog::getDescription() const { return mDescription; } ESM::Class::Specialization CreateClassDialog::getSpecializationId() const { return mSpecializationId; } std::vector CreateClassDialog::getFavoriteAttributes() const { std::vector v; v.push_back(mFavoriteAttribute0->getAttributeId()); v.push_back(mFavoriteAttribute1->getAttributeId()); return v; } std::vector CreateClassDialog::getMajorSkills() const { std::vector v; v.reserve(mMajorSkill.size()); for (const auto& widget : mMajorSkill) { v.push_back(widget->getSkillId()); } return v; } std::vector CreateClassDialog::getMinorSkills() const { std::vector v; v.reserve(mMinorSkill.size()); for (const auto& widget : mMinorSkill) { v.push_back(widget->getSkillId()); } return v; } void CreateClassDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } // widget controls void CreateClassDialog::onDialogCancel() { 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) { mSpecDialog = std::make_unique(); mSpecDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSpecDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSpecializationSelected); mSpecDialog->setVisible(true); } void CreateClassDialog::onSpecializationSelected() { mSpecializationId = mSpecDialog->getSpecializationId(); setSpecialization(mSpecializationId); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSpecDialog)); } void CreateClassDialog::setSpecialization(int id) { 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) { mAttribDialog = std::make_unique(); mAffectedAttribute = _sender; mAttribDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mAttribDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onAttributeSelected); mAttribDialog->setVisible(true); } void CreateClassDialog::onAttributeSelected() { ESM::RefId id = mAttribDialog->getAttributeId(); if (mAffectedAttribute == mFavoriteAttribute0) { if (mFavoriteAttribute1->getAttributeId() == id) mFavoriteAttribute1->setAttributeId(mFavoriteAttribute0->getAttributeId()); } else if (mAffectedAttribute == mFavoriteAttribute1) { if (mFavoriteAttribute0->getAttributeId() == id) mFavoriteAttribute0->setAttributeId(mFavoriteAttribute1->getAttributeId()); } mAffectedAttribute->setAttributeId(id); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mAttribDialog)); update(); } void CreateClassDialog::onSkillClicked(Widgets::MWSkillPtr _sender) { mSkillDialog = std::make_unique(); mAffectedSkill = _sender; mSkillDialog->eventCancel += MyGUI::newDelegate(this, &CreateClassDialog::onDialogCancel); mSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &CreateClassDialog::onSkillSelected); mSkillDialog->setVisible(true); } void CreateClassDialog::onSkillSelected() { ESM::RefId id = mSkillDialog->getSkillId(); // Avoid duplicate skills by swapping any skill field that matches the selected one for (Widgets::MWSkillPtr& skill : mSkills) { if (skill == mAffectedSkill) continue; if (skill->getSkillId() == id) { skill->setSkillId(mAffectedSkill->getSkillId()); break; } } mAffectedSkill->setSkillId(mSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSkillDialog)); update(); } void CreateClassDialog::onDescriptionClicked(MyGUI::Widget* _sender) { mDescDialog = std::make_unique(); mDescDialog->setTextInput(mDescription); mDescDialog->eventDone += MyGUI::newDelegate(this, &CreateClassDialog::onDescriptionEntered); mDescDialog->setVisible(true); } void CreateClassDialog::onDescriptionEntered(WindowBase* parWindow) { mDescription = mDescDialog->getTextInput(); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mDescDialog)); } void CreateClassDialog::onOkClicked(MyGUI::Widget* _sender) { if (getName().size() <= 0) return; eventDone(this); } void CreateClassDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } /* SelectSpecializationDialog */ SelectSpecializationDialog::SelectSpecializationDialog() : WindowModal("openmw_chargen_select_specialization.layout") { // Centre dialog center(); 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], {}) }; mSpecialization0->setCaption(combat); mSpecialization0->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization1->setCaption(magic); mSpecialization1->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecialization2->setCaption(stealth); mSpecialization2->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onSpecializationClicked); mSpecializationId = ESM::Class::Combat; ToolTips::createSpecializationToolTip(mSpecialization0, combat, ESM::Class::Combat); ToolTips::createSpecializationToolTip(mSpecialization1, magic, ESM::Class::Magic); ToolTips::createSpecializationToolTip(mSpecialization2, stealth, ESM::Class::Stealth); MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSpecializationDialog::onCancelClicked); } SelectSpecializationDialog::~SelectSpecializationDialog() {} // widget controls void SelectSpecializationDialog::onSpecializationClicked(MyGUI::Widget* _sender) { if (_sender == mSpecialization0) mSpecializationId = ESM::Class::Combat; else if (_sender == mSpecialization1) mSpecializationId = ESM::Class::Magic; else if (_sender == mSpecialization2) mSpecializationId = ESM::Class::Stealth; else return; eventItemSelected(); } void SelectSpecializationDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectSpecializationDialog::exit() { eventCancel(); return true; } /* SelectAttributeDialog */ SelectAttributeDialog::SelectAttributeDialog() : WindowModal("openmw_chargen_select_attribute.layout") , mAttributeId(ESM::Attribute::Strength) { // Centre dialog center(); 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) { 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); } // widget controls void SelectAttributeDialog::onAttributeClicked(Widgets::MWAttributePtr _sender) { mAttributeId = _sender->getAttributeId(); eventItemSelected(); } void SelectAttributeDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectAttributeDialog::exit() { eventCancel(); return true; } /* SelectSkillDialog */ SelectSkillDialog::SelectSkillDialog() : WindowModal("openmw_chargen_select_skill.layout") , mSkillId(ESM::Skill::Block) { // Centre dialog center(); 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) { coord.width = widget->getCoord().width; coord.height = 18; while (widget->getChildCount() > 0) MyGUI::Gui::getInstance().destroyWidget(widget->getChildAt(0)); } for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { 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; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SelectSkillDialog::onCancelClicked); } SelectSkillDialog::~SelectSkillDialog() {} // widget controls void SelectSkillDialog::onSkillClicked(Widgets::MWSkillPtr _sender) { mSkillId = _sender->getSkillId(); eventItemSelected(); } void SelectSkillDialog::onCancelClicked(MyGUI::Widget* _sender) { exit(); } bool SelectSkillDialog::exit() { eventCancel(); return true; } /* DescriptionDialog */ DescriptionDialog::DescriptionDialog() : WindowModal("openmw_chargen_class_description.layout") { // Centre dialog center(); getWidget(mTextEdit, "TextEdit"); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DescriptionDialog::onOkClicked); 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() {} // widget controls void DescriptionDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void setClassImage(MyGUI::ImageBox* imageBox, const ESM::RefId& classId) { std::string_view fallback = "textures\\levelup\\warrior.dds"; std::string classImage; if (const auto* id = classId.getIf()) { 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); } } openmw-openmw-0.49.0/apps/openmw/mwgui/class.hpp000066400000000000000000000221251503074453300216400ustar00rootroot00000000000000#ifndef MWGUI_CLASS_H #define MWGUI_CLASS_H #include #include #include #include #include #include #include "widgets.hpp" #include "windowbase.hpp" namespace MWGui { void setClassImage(MyGUI::ImageBox* imageBox, const ESM::RefId& classId); class InfoBoxDialog : public WindowModal { public: InfoBoxDialog(); typedef std::vector ButtonList; void setText(const std::string& str); std::string getText() const; void setButtons(ButtonList& buttons); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::MultiDelegate EventHandle_Int; /** Event : Button was clicked.\n signature : void method(int index)\n */ EventHandle_Int eventButtonSelected; protected: void onButtonClicked(MyGUI::Widget* _sender); private: void fitToText(MyGUI::TextBox* widget); void layoutVertically(MyGUI::Widget* widget, int margin); MyGUI::Widget* mTextBox; MyGUI::TextBox* mText; MyGUI::Widget* mButtonBar; std::vector mButtons; }; // Lets the player choose between 3 ways of creating a class class ClassChoiceDialog : public InfoBoxDialog { public: // Corresponds to the buttons that can be clicked enum ClassChoice { Class_Generate = 0, Class_Pick = 1, Class_Create = 2, Class_Back = 3 }; ClassChoiceDialog(); }; class GenerateClassResultDialog : public WindowModal { public: GenerateClassResultDialog(); void setClassId(const ESM::RefId& classId); bool exit() override { return false; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: MyGUI::ImageBox* mClassImage; MyGUI::TextBox* mClassName; ESM::RefId mCurrentClassId; }; class PickClassDialog : public WindowModal { public: PickClassDialog(); const ESM::RefId& getClassId() const { return mCurrentClassId; } void setClassId(const ESM::RefId& classId); void setNextButtonShow(bool shown); void onOpen() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onSelectClass(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateClasses(); void updateStats(); MyGUI::ImageBox* mClassImage; MyGUI::ListBox* mClassList; MyGUI::TextBox* mSpecializationName; Widgets::MWAttributePtr mFavoriteAttribute[2]; Widgets::MWSkillPtr mMajorSkill[5]; Widgets::MWSkillPtr mMinorSkill[5]; ESM::RefId mCurrentClassId; }; class SelectSpecializationDialog : public WindowModal { public: SelectSpecializationDialog(); ~SelectSpecializationDialog(); bool exit() override; ESM::Class::Specialization getSpecializationId() const { return mSpecializationId; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, specialization selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onSpecializationClicked(MyGUI::Widget* _sender); void onCancelClicked(MyGUI::Widget* _sender); private: MyGUI::TextBox *mSpecialization0, *mSpecialization1, *mSpecialization2; ESM::Class::Specialization mSpecializationId; }; class SelectAttributeDialog : public WindowModal { public: SelectAttributeDialog(); ~SelectAttributeDialog() override = default; bool exit() override; ESM::RefId getAttributeId() const { return mAttributeId; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, attribute selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onAttributeClicked(Widgets::MWAttributePtr _sender); void onCancelClicked(MyGUI::Widget* _sender); private: ESM::RefId mAttributeId; }; class SelectSkillDialog : public WindowModal { public: SelectSkillDialog(); ~SelectSkillDialog(); bool exit() override; ESM::RefId getSkillId() const { return mSkillId; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Cancel button clicked.\n signature : void method()\n */ EventHandle_Void eventCancel; /** Event : Dialog finished, skill selected.\n signature : void method()\n */ EventHandle_Void eventItemSelected; protected: void onSkillClicked(Widgets::MWSkillPtr _sender); void onCancelClicked(MyGUI::Widget* _sender); private: ESM::RefId mSkillId; }; class DescriptionDialog : public WindowModal { public: DescriptionDialog(); ~DescriptionDialog(); std::string getTextInput() const { return mTextEdit->getCaption(); } void setTextInput(const std::string& text) { mTextEdit->setCaption(text); } /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); private: MyGUI::EditBox* mTextEdit; }; class CreateClassDialog : public WindowModal { public: CreateClassDialog(); virtual ~CreateClassDialog(); bool exit() override { return false; } 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; void setNextButtonShow(bool shown); // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); void onSpecializationClicked(MyGUI::Widget* _sender); void onSpecializationSelected(); void onAttributeClicked(Widgets::MWAttributePtr _sender); void onAttributeSelected(); void onSkillClicked(Widgets::MWSkillPtr _sender); void onSkillSelected(); void onDescriptionClicked(MyGUI::Widget* _sender); void onDescriptionEntered(WindowBase* parWindow); void onDialogCancel(); void setSpecialization(int id); void update(); private: MyGUI::EditBox* mEditName; MyGUI::TextBox* mSpecializationName; Widgets::MWAttributePtr mFavoriteAttribute0, mFavoriteAttribute1; std::array mMajorSkill; std::array mMinorSkill; std::vector mSkills; std::string mDescription; std::unique_ptr mSpecDialog; std::unique_ptr mAttribDialog; std::unique_ptr mSkillDialog; std::unique_ptr mDescDialog; ESM::Class::Specialization mSpecializationId; Widgets::MWAttributePtr mAffectedAttribute; Widgets::MWSkillPtr mAffectedSkill; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/companionitemmodel.cpp000066400000000000000000000033661503074453300244170ustar00rootroot00000000000000#include "companionitemmodel.hpp" #include "../mwworld/class.hpp" namespace { void modifyProfit(const MWWorld::Ptr& actor, int diff) { const ESM::RefId& script = actor.getClass().getScript(actor); if (!script.empty()) { int profit = actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); profit += diff; actor.getRefData().getLocals().setVarByInt(script, "minimumprofit", profit); } } } namespace MWGui { CompanionItemModel::CompanionItemModel(const MWWorld::Ptr& actor) : InventoryItemModel(actor) { } 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); return InventoryItemModel::copyItem(item, count, allowAutoEquip); } void CompanionItemModel::removeItem(const ItemStack& item, size_t count) { if (hasProfit(mActor)) modifyProfit(mActor, -item.mBase.getClass().getValue(item.mBase) * count); InventoryItemModel::removeItem(item, count); } bool CompanionItemModel::hasProfit(const MWWorld::Ptr& actor) { const ESM::RefId& script = actor.getClass().getScript(actor); if (script.empty()) return false; return actor.getRefData().getLocals().hasVar(script, "minimumprofit"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/companionitemmodel.hpp000066400000000000000000000014011503074453300244100ustar00rootroot00000000000000#ifndef MWGUI_COMPANION_ITEM_MODEL_H #define MWGUI_COMPANION_ITEM_MODEL_H #include "inventoryitemmodel.hpp" namespace MWGui { /// @brief The companion item model keeps track of the companion's profit by /// monitoring which items are being added to and removed from the model. class CompanionItemModel : public InventoryItemModel { public: CompanionItemModel(const MWWorld::Ptr& actor); 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/companionwindow.cpp000066400000000000000000000147051503074453300237460ustar00rootroot00000000000000#include "companionwindow.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "companionitemmodel.hpp" #include "countdialog.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) { const ESM::RefId& script = actor.getClass().getScript(actor); if (!script.empty()) { return actor.getRefData().getLocals().getIntVar(script, "minimumprofit"); } return 0; } } 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) { mDragAndDrop->drop(mModel, mItemView); updateEncumbranceBar(); return; } const ItemStack& item = mSortModel->getItem(index); // We can't take conjured items from a companion actor if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog12}"); return; } 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); } 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) { mDragAndDrop->drop(mModel, mItemView); updateEncumbranceBar(); } } 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)); } 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)); if (mModel && mModel->hasProfit(mPtr)) { mProfitLabel->setCaptionWithReplacing("#{sProfitValue} " + MyGUI::utility::toString(getProfit(mPtr))); } 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; } void CompanionWindow::onMessageBoxButtonClicked(int button) { if (button == 0) { 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; } } openmw-openmw-0.49.0/apps/openmw/mwgui/companionwindow.hpp000066400000000000000000000031461503074453300237500ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_COMPANIONWINDOW_H #define OPENMW_MWGUI_COMPANIONWINDOW_H #include "referenceinterface.hpp" #include "windowbase.hpp" namespace MWGui { namespace Widgets { class MWDynamicStat; } class MessageBoxManager; class ItemView; class DragAndDrop; class SortFilterItemModel; class CompanionItemModel; class CompanionWindow : public WindowBase, public ReferenceInterface { public: CompanionWindow(DragAndDrop* dragAndDrop, MessageBoxManager* manager); bool exit() override; void resetReference() 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; CompanionItemModel* mModel; int mSelectedItem; DragAndDrop* mDragAndDrop; MyGUI::Button* mCloseButton; MyGUI::EditBox* mFilterEdit; MyGUI::TextBox* mProfitLabel; Widgets::MWDynamicStat* mEncumbranceBar; MessageBoxManager* mMessageBoxManager; void onItemSelected(int index); void onNameFilterChanged(MyGUI::EditBox* _sender); void onBackgroundSelected(); void dragItem(MyGUI::Widget* sender, int count); void onMessageBoxButtonClicked(int button); void updateEncumbranceBar(); void onCloseButtonClicked(MyGUI::Widget* _sender); void onReferenceUnavailable() override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/confirmationdialog.cpp000066400000000000000000000030221503074453300243710ustar00rootroot00000000000000#include "confirmationdialog.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { ConfirmationDialog::ConfirmationDialog() : WindowModal("openmw_confirmation_dialog.layout") { getWidget(mMessage, "Message"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ConfirmationDialog::onOkButtonClicked); } void ConfirmationDialog::askForConfirmation(const std::string& message) { setVisible(true); mMessage->setCaptionWithReplacing(message); int height = mMessage->getTextSize().height + 60; int width = mMessage->getTextSize().width + 24; mMainWidget->setSize(width, height); mMessage->setSize(mMessage->getWidth(), mMessage->getTextSize().height + 24); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); center(); } bool ConfirmationDialog::exit() { setVisible(false); eventCancelClicked(); return true; } void ConfirmationDialog::onCancelButtonClicked(MyGUI::Widget* _sender) { exit(); } void ConfirmationDialog::onOkButtonClicked(MyGUI::Widget* _sender) { setVisible(false); eventOkClicked(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/confirmationdialog.hpp000066400000000000000000000014541503074453300244050ustar00rootroot00000000000000#ifndef MWGUI_CONFIRMATIONDIALOG_H #define MWGUI_CONFIRMATIONDIALOG_H #include "windowbase.hpp" namespace MWGui { class ConfirmationDialog : public WindowModal { 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/console.cpp000066400000000000000000000712341503074453300221750ustar00rootroot00000000000000#include "console.hpp" #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 "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" namespace MWGui { class ConsoleInterpreterContext : public MWScript::InterpreterContext { Console& mConsole; public: ConsoleInterpreterContext(Console& console, MWWorld::Ptr reference); 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) { } void ConsoleInterpreterContext::report(const std::string& message) { mConsole.printOK(message); } bool Console::compile(const std::string& cmd, Compiler::Output& output) { try { ErrorHandler::reset(); std::istringstream input(cmd + '\n'); Compiler::Scanner scanner(*this, input, mCompilerContext.getExtensions()); Compiler::LineParser parser( *this, mCompilerContext, output.getLocals(), output.getLiterals(), output.getCode(), true); scanner.scan(parser); return isGood(); } catch (const Compiler::SourceException&) { // error has already been reported via error handler } catch (const std::exception& error) { printError(std::string("Error: ") + error.what()); } return false; } 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); } void Console::report(const std::string& message, Type type) { printError((type == ErrorMessage ? "error: " : "warning: ") + message); } void Console::listNames() { if (mNames.empty()) { // keywords std::istringstream input; Compiler::Scanner scanner(*this, input, mCompilerContext.getExtensions()); scanner.listKeywords(mNames); // identifier const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); std::vector ids; for (const auto* store : esmStore) { store->listIdentifier(ids); for (auto id : ids) { if (id.is()) mNames.push_back(id.getRefIdString()); } ids.clear(); } // 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()); // remove duplicates mNames.erase(std::unique(mNames.begin(), mNames.end()), mNames.end()); } } 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); 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::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); // command history file initConsoleHistory(); } Console::~Console() { if (mCommandHistoryFile && mCommandHistoryFile.is_open()) mCommandHistoryFile.close(); } void Console::onOpen() { // Give keyboard focus to the combo box whenever the console is // turned on and place it over other widgets MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); MyGUI::LayerManager::getInstance().upLayerItem(mMainWidget); } void Console::print(const std::string& msg, std::string_view color) { mHistory->addText(std::string(color) + MyGUI::TextIterator::toTagsString(msg)); } void Console::printOK(const std::string& msg) { print(msg + "\n", MWBase::WindowManager::sConsoleColor_Success); } void Console::printError(const std::string& msg) { print(msg + "\n", MWBase::WindowManager::sConsoleColor_Error); } void Console::execute(const std::string& command) { // Log the command 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()) { const ESM::RefId& script = mPtr.getClass().getScript(mPtr); if (!script.empty()) locals = MWBase::Environment::get().getScriptManager()->getLocals(script); } Compiler::Output output(locals); if (compile(command + "\n", output)) { try { ConsoleInterpreterContext interpreterContext(*this, mPtr); Interpreter::Interpreter interpreter; 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()); } } } void Console::executeFile(const std::filesystem::path& path) { std::ifstream stream(path); if (!stream.is_open()) { 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() { resetReference(); } bool isWhitespace(char c) { return c == ' ' || c == '\t'; } void Console::commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char) { if (MyGUI::InputManager::getInstance().isControlPressed()) { if (key == MyGUI::KeyCode::W) { auto caption = mCommandLine->getOnlyText(); if (caption.empty()) return; size_t max = mCommandLine->getTextCursor(); while (max > 0 && (isWhitespace(caption[max - 1]) || caption[max - 1] == '>')) max--; while (max > 0 && !isWhitespace(caption[max - 1]) && caption[max - 1] != '>') max--; size_t length = mCommandLine->getTextCursor() - max; if (length > 0) { caption.erase(max, length); mCommandLine->setOnlyText(caption); mCommandLine->setTextCursor(max); } } else if (key == MyGUI::KeyCode::U) { if (mCommandLine->getTextCursor() > 0) { auto text = mCommandLine->getOnlyText(); text.erase(0, mCommandLine->getTextCursor()); mCommandLine->setOnlyText(text); mCommandLine->setTextCursor(0); } } } else if (key == MyGUI::KeyCode::Tab && mConsoleMode.empty()) { std::vector matches; listNames(); 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) { if (i == 50) break; printOK(match); i++; } } } if (mCommandHistory.empty()) return; // Traverse history with up and down arrows if (key == MyGUI::KeyCode::ArrowUp) { // If the user was editing a string, store it for later if (mCurrent == mCommandHistory.end()) mEditString = mCommandLine->getOnlyText(); if (mCurrent != mCommandHistory.begin()) { --mCurrent; mCommandLine->setOnlyText(*mCurrent); } } else if (key == MyGUI::KeyCode::ArrowDown) { if (mCurrent != mCommandHistory.end()) { ++mCurrent; if (mCurrent != mCommandHistory.end()) mCommandLine->setOnlyText(*mCurrent); else // Restore the edit string mCommandLine->setOnlyText(mEditString); } } } void Console::acceptCommand(MyGUI::EditBox* _sender) { 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 // during the actual command execution 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 this isn't the first search, adjust the range based on the previous occurrence. if (mCurrentOccurrenceIndex != std::string::npos) { if (direction == SearchDirection::Forward) { firstIndex = mCurrentOccurrenceIndex + mCurrentOccurrenceLength; } else if (direction == SearchDirection::Reverse) { lastIndex = mCurrentOccurrenceIndex; } } 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 (lastIndex <= firstIndex) { mCurrentOccurrenceIndex = std::string::npos; mCurrentOccurrenceLength = 0; return; } 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; 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); // 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 output = input; std::string tmp = input; bool has_front_quote = false; /* Does the input string contain things that don't have to be completed? If yes erase them. */ /* Erase a possible call to an explicit reference. */ size_t explicitPos = tmp.find("->"); if (explicitPos != std::string::npos) { 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 == '"') numquotes++; } /* Is it terminated?*/ if (numquotes % 2) { tmp.erase(0, tmp.rfind('"') + 1); has_front_quote = true; } else { size_t pos; if ((((pos = tmp.rfind(' ')) != std::string::npos)) && (pos > tmp.rfind('"'))) { tmp.erase(0, tmp.rfind(' ') + 1); } else { tmp.clear(); } has_front_quote = false; } } /* No quotation marks. Are there spaces?*/ else { size_t rpos; if ((rpos = tmp.rfind(' ')) != std::string::npos) { if (rpos == 0) { tmp.clear(); } 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()); /* 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) { bool string_different = false; /* Is the string shorter than the input string? If yes skip it. */ 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; break; } } if (string_different) continue; /* The beginning of the string matches the input string, save it for the next test. */ matches.push_back(name); } /* There are no matches. Return the unchanged input. */ if (matches.empty()) { return input; } /* Only one match. We're done. */ 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 += '"'; return output.append(matches.front() + std::string("\" ")); } else if (has_front_quote) { return output.append(matches.front() + std::string("\" ")); } else { return output.append(matches.front() + std::string(" ")); } } /* 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& match : matches) { 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)); return output; } } } /* All keywords match with the shortest. Append it to the output string and return it. */ return output.append(matches.front()); } void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { if (mPtr == currentPtr) mPtr = newPtr; } void Console::setSelectedObject(const MWWorld::Ptr& object) { if (!object.isEmpty()) { if (object == mPtr) mPtr = MWWorld::Ptr(); else mPtr = object; // User clicked on an object. Restore focus to the console command line. MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); } else 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() { setSelectedObject(MWWorld::Ptr()); } void Console::resetReference() { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/console.hpp000066400000000000000000000111211503074453300221670ustar00rootroot00000000000000#ifndef MWGUI_CONSOLE_H #define MWGUI_CONSOLE_H #include #include #include #include #include #include #include #include "../mwbase/windowmanager.hpp" #include "../mwscript/compilercontext.hpp" #include "referenceinterface.hpp" #include "windowbase.hpp" 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); 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; typedef std::list StringList; // History of previous entered commands StringList mCommandHistory; StringList::iterator mCurrent; std::string mEditString; std::ofstream mCommandHistoryFile; Console(int w, int h, bool consoleOnlyScripts, Files::ConfigurationManager& cfgMgr); ~Console(); 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); // These are pre-colored versions that you should use. /// Output from successful console command void printOK(const std::string& msg); /// Error message void printError(const std::string& msg); void execute(const std::string& command); void executeFile(const std::filesystem::path& path); void updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr); void onFrame(float dt) override { checkReferenceAvailable(); } void clear() override; void resetReference() override; const std::string& getConsoleMode() const { return mConsoleMode; } void setConsoleMode(std::string_view mode); protected: void onReferenceUnavailable() override; private: std::string mConsoleMode; void updateConsoleTitle(); void commandBoxKeyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); void acceptCommand(MyGUI::EditBox* _sender); 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; std::string complete(std::string input, std::vector& matches); Compiler::Extensions mExtensions; MWScript::CompilerContext mCompilerContext; std::vector mNames; bool mConsoleOnlyScripts; Files::ConfigurationManager& mCfgMgr; 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 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(); void initConsoleHistory(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/container.cpp000066400000000000000000000271161503074453300225150ustar00rootroot00000000000000#include "container.hpp" #include #include #include "../mwbase/environment.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" #include "../mwmechanics/aipackage.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/summoning.hpp" #include "../mwscript/interpretercontext.hpp" #include "countdialog.hpp" #include "inventorywindow.hpp" #include "containeritemmodel.hpp" #include "draganddrop.hpp" #include "inventoryitemmodel.hpp" #include "itemview.hpp" #include "pickpocketitemmodel.hpp" #include "sortfilteritemmodel.hpp" #include "tooltips.hpp" namespace MWGui { ContainerWindow::ContainerWindow(DragAndDrop* dragAndDrop) : WindowBase("openmw_container_window.layout") , mDragAndDrop(dragAndDrop) , mSortModel(nullptr) , mModel(nullptr) , mSelectedItem(-1) , mTreatNextOpenAsLoot(false) { getWidget(mDisposeCorpseButton, "DisposeCorpseButton"); getWidget(mTakeButton, "TakeButton"); getWidget(mCloseButton, "CloseButton"); getWidget(mItemView, "ItemView"); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &ContainerWindow::onBackgroundSelected); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ContainerWindow::onItemSelected); 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); } void ContainerWindow::onItemSelected(int index) { if (mDragAndDrop->mIsOnDragAndDrop) { dropItem(); return; } const ItemStack& item = mSortModel->getItem(index); // We can't take a conjured item from a container (some NPC we're pickpocketing, a box, etc) if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage1}"); return; } 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, &ContainerWindow::dragItem); } else dragItem(nullptr, count); } void ContainerWindow::dragItem(MyGUI::Widget* sender, int count) { if (!mModel) return; if (!onTakeItem(mModel->getItem(mSelectedItem), count)) return; mDragAndDrop->startDrag(mSelectedItem, mSortModel, mModel, mItemView, count); } void ContainerWindow::dropItem() { if (!mModel) return; bool success = mModel->onDropItem(mDragAndDrop->mItem.mBase, mDragAndDrop->mDraggedCount); if (success) mDragAndDrop->drop(mModel, mItemView); } void ContainerWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) dropItem(); } 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 && !lootAnyway) { // we are stealing stuff model = std::make_unique(mPtr, std::make_unique(container), !mPtr.getClass().getCreatureStats(mPtr).getKnockedDown()); } else model = std::make_unique(container); } else { model = std::make_unique(container); } mDisposeCorpseButton->setVisible(loot); mModel = model.get(); auto sortModel = std::make_unique(std::move(model)); mSortModel = sortModel.get(); mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); setTitle(container.getClass().getName(container)); } void ContainerWindow::resetReference() { ReferenceInterface::resetReference(); mItemView->setModel(nullptr); mModel = nullptr; mSortModel = nullptr; } void ContainerWindow::onClose() { WindowBase::onClose(); // Make sure the window was actually closed and not temporarily hidden. if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Container)) return; if (mModel) mModel->onClose(); if (!mPtr.isEmpty()) MWBase::Environment::get().getMechanicsManager()->onClose(mPtr); resetReference(); } void ContainerWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } void ContainerWindow::onTakeAllButtonClicked(MyGUI::Widget* _sender) { if (!mModel) return; if (mDragAndDrop != nullptr && mDragAndDrop->mIsOnDragAndDrop) return; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); // transfer everything into the player's inventory ItemModel* playerModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getModel(); assert(mModel); mModel->update(); // unequip all items to avoid unequipping/reequipping if (mPtr.getClass().hasInventoryStore(mPtr)) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); 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); } } mModel->update(); for (size_t i = 0; i < mModel->getItemCount(); ++i) { if (i == 0) { // play the sound of the first object MWWorld::Ptr item = mModel->getItem(i).mBase; const ESM::RefId& sound = item.getClass().getUpSoundId(item); MWBase::Environment::get().getWindowManager()->playSound(sound); } const ItemStack& item = mModel->getItem(i); if (!onTakeItem(item, item.mCount)) break; mModel->moveItem(item, item.mCount, playerModel); } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } void ContainerWindow::onDisposeCorpseButtonClicked(MyGUI::Widget* sender) { if (mDragAndDrop == nullptr || !mDragAndDrop->mIsOnDragAndDrop) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCloseButton); // Copy mPtr because onTakeAllButtonClicked closes the window which resets the reference MWWorld::Ptr ptr = mPtr; onTakeAllButtonClicked(mTakeButton); if (ptr.getClass().isPersistent(ptr)) MWBase::Environment::get().getWindowManager()->messageBox("#{sDisposeCorpseFail}"); else { MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); // If we dispose corpse before end of death animation, we should update death counter counter manually. // Also we should run actor's script - it may react on actor's death. if (creatureStats.isDead() && !creatureStats.isDeathAnimationFinished()) { creatureStats.setDeathAnimationFinished(true); MWBase::Environment::get().getMechanicsManager()->notifyDied(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); } // Clean up summoned creatures as well 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()) { 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 summon = *it; summons.erase(it); MWMechanics::purgeSummonEffect(summoner, summon); break; } } } } MWBase::Environment::get().getWorld()->deleteObject(ptr); } mPtr = MWWorld::Ptr(); } } void ContainerWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } bool ContainerWindow::onTakeItem(const ItemStack& item, int count) { return mModel->onTakeItem(item.mBase, count); } void ContainerWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) { if (mModel && mModel->usesContainer(ptr)) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Container); } } openmw-openmw-0.49.0/apps/openmw/mwgui/container.hpp000066400000000000000000000034111503074453300225120ustar00rootroot00000000000000#ifndef MGUI_CONTAINER_H #define MGUI_CONTAINER_H #include "referenceinterface.hpp" #include "windowbase.hpp" #include "itemmodel.hpp" namespace MyGUI { class Gui; class Widget; } namespace MWGui { class ContainerWindow; class ItemView; class SortFilterItemModel; } namespace MWGui { class ContainerWindow : public WindowBase, public ReferenceInterface { public: ContainerWindow(DragAndDrop* dragAndDrop); void setPtr(const MWWorld::Ptr& container) override; void onClose() override; void clear() override { resetReference(); } void onFrame(float dt) override { checkReferenceAvailable(); } 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; MWGui::ItemView* mItemView; SortFilterItemModel* mSortModel; ItemModel* mModel; int mSelectedItem; bool mTreatNextOpenAsLoot; MyGUI::Button* mDisposeCorpseButton; MyGUI::Button* mTakeButton; MyGUI::Button* mCloseButton; void onItemSelected(int index); void onBackgroundSelected(); void dragItem(MyGUI::Widget* sender, int count); void dropItem(); void onCloseButtonClicked(MyGUI::Widget* _sender); void onTakeAllButtonClicked(MyGUI::Widget* _sender); void onDisposeCorpseButtonClicked(MyGUI::Widget* sender); /// @return is taking the item allowed? bool onTakeItem(const ItemStack& item, int count); void onReferenceUnavailable() override; }; } #endif // CONTAINER_H openmw-openmw-0.49.0/apps/openmw/mwgui/containeritemmodel.cpp000066400000000000000000000221611503074453300244100ustar00rootroot00000000000000#include "containeritemmodel.hpp" #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" namespace { 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); if (left.getContainerStore()) return left.getContainerStore()->stacks(left, right); if (right.getContainerStore()) return right.getContainerStore()->stacks(left, right); MWWorld::ContainerStore store; return store.stacks(left, right); } } 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) { 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; // 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); } 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]; } size_t ContainerItemModel::getItemCount() { return mItems.size(); } ItemModel::ModelIndex ContainerItemModel::getIndex(const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) { if (itemStack == item) return i; ++i; } return -1; } 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); } 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 (auto& source : mItemSources) { MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { 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 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"); } void ContainerItemModel::update() { mItems.clear(); for (auto& source : mItemSources) { MWWorld::ContainerStore& store = source.first.getClass().getContainerStore(source.first); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (!(*it).getClass().showsInInventory(*it)) continue; 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; } } if (!found) { // no stack yet, create one ItemStack newItem(*it, this, it->getCellRef().getCount()); mItems.push_back(newItem); } } } for (MWWorld::Ptr& source : mWorldItems) { bool found = false; for (ItemStack& itemStack : mItems) { if (stacks(source, itemStack.mBase)) { // we already have an item stack of this kind, add to it itemStack.mCount += source.getCellRef().getCount(); found = true; break; } } if (!found) { // no stack yet, create one ItemStack newItem(source, this, source.getCellRef().getCount()); mItems.push_back(newItem); } } } bool ContainerItemModel::onDropItem(const MWWorld::Ptr& item, int count) { 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) { 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) { MWBase::Environment::get().getWindowManager()->messageBox("#{sContentsMessage3}"); return false; } return true; } bool ContainerItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { if (mItemSources.empty()) return false; MWWorld::Ptr target = mItemSources[0].first; // 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; } } openmw-openmw-0.49.0/apps/openmw/mwgui/containeritemmodel.hpp000066400000000000000000000033521503074453300244160ustar00rootroot00000000000000#ifndef MWGUI_CONTAINER_ITEM_MODEL_H #define MWGUI_CONTAINER_ITEM_MODEL_H #include #include #include "itemmodel.hpp" #include "../mwworld/containerstore.hpp" namespace MWGui { /// @brief The container item model supports multiple item sources, which are needed for /// making NPCs sell items from containers owned by them 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, /// while the last element will be used to add new items to. 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; ItemStack getItem(ModelIndex index) override; ModelIndex getIndex(const ItemStack& item) override; size_t getItemCount() 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; const bool mTrading; std::vector mItems; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/controllers.cpp000066400000000000000000000006651503074453300231010ustar00rootroot00000000000000#include "controllers.hpp" #include #include namespace MWGui { namespace Controllers { void ControllerFollowMouse::prepareItem(MyGUI::Widget* _widget) {} bool ControllerFollowMouse::addTime(MyGUI::Widget* _widget, float _time) { _widget->setPosition(MyGUI::InputManager::getInstance().getMousePosition()); return true; } } } openmw-openmw-0.49.0/apps/openmw/mwgui/controllers.hpp000066400000000000000000000010201503074453300230700ustar00rootroot00000000000000#ifndef MWGUI_CONTROLLERS_H #define MWGUI_CONTROLLERS_H #include #include namespace MyGUI { class Widget; } namespace MWGui::Controllers { /// Automatically positions a widget below the mouse cursor. class ControllerFollowMouse final : public MyGUI::ControllerItem { MYGUI_RTTI_DERIVED(ControllerFollowMouse) private: bool addTime(MyGUI::Widget* _widget, float _time) override; void prepareItem(MyGUI::Widget* _widget) override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/countdialog.cpp000066400000000000000000000060231503074453300230350ustar00rootroot00000000000000#include "countdialog.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { CountDialog::CountDialog() : WindowModal("openmw_count_window.layout") { getWidget(mSlider, "CountSlider"); getWidget(mItemEdit, "ItemEdit"); getWidget(mItemText, "ItemText"); getWidget(mLabelText, "LabelText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &CountDialog::onOkButtonClicked); mItemEdit->eventValueChanged += MyGUI::newDelegate(this, &CountDialog::onEditValueChanged); mSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &CountDialog::onSliderMoved); // make sure we read the enter key being pressed to accept multiple items mItemEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &CountDialog::onEnterKeyPressed); } void CountDialog::openCountDialog(const std::string& item, const std::string& message, const int maxCount) { setVisible(true); mLabelText->setCaptionWithReplacing(message); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); mSlider->setScrollRange(maxCount); mItemText->setCaption(item); 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); mItemEdit->setMinValue(1); mItemEdit->setMaxValue(maxCount); mItemEdit->setValue(maxCount); } void CountDialog::onCancelButtonClicked(MyGUI::Widget* _sender) { setVisible(false); } void CountDialog::onOkButtonClicked(MyGUI::Widget* _sender) { eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); setVisible(false); } // essentially duplicating what the OK button does if user presses // Enter key void CountDialog::onEnterKeyPressed(MyGUI::EditBox* _sender) { eventOkClicked(nullptr, mSlider->getScrollPosition() + 1); setVisible(false); // To do not spam onEnterKeyPressed() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void CountDialog::onEditValueChanged(int value) { mSlider->setScrollPosition(value - 1); } void CountDialog::onSliderMoved(MyGUI::ScrollBar* _sender, size_t _position) { mItemEdit->setValue(_position + 1); } } openmw-openmw-0.49.0/apps/openmw/mwgui/countdialog.hpp000066400000000000000000000021711503074453300230420ustar00rootroot00000000000000#ifndef MWGUI_COUNTDIALOG_H #define MWGUI_COUNTDIALOG_H #include "windowbase.hpp" namespace Gui { class NumericEditBox; } namespace MWGui { class CountDialog : public WindowModal { 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/cursor.cpp000066400000000000000000000037341503074453300220500ustar00rootroot00000000000000#include "cursor.hpp" #include #include #include #include namespace MWGui { ResourceImageSetPointerFix::ResourceImageSetPointerFix() : mImageSet(nullptr) , mRotation(0) { } ResourceImageSetPointerFix::~ResourceImageSetPointerFix() {} void ResourceImageSetPointerFix::deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) { Base::deserialization(_node, _version); MyGUI::xml::ElementEnumerator info = _node->getElementEnumerator(); while (info.next("Property")) { auto key = info->findAttribute("key"); auto value = info->findAttribute("value"); if (key == "Point") mPoint = MyGUI::IntPoint::parse(value); else if (key == "Size") mSize = MyGUI::IntSize::parse(value); else if (key == "Rotation") mRotation = MyGUI::utility::parseInt(value); else if (key == "Resource") mImageSet = MyGUI::ResourceManager::getInstance().getByName(value)->castType(); } } int ResourceImageSetPointerFix::getRotation() { return mRotation; } void ResourceImageSetPointerFix::setImage(MyGUI::ImageBox* _image) { if (mImageSet != nullptr) _image->setItemResourceInfo(mImageSet->getIndexInfo(0, 0)); } void ResourceImageSetPointerFix::setPosition(MyGUI::ImageBox* _image, const MyGUI::IntPoint& _point) { _image->setCoord(_point.left - mPoint.left, _point.top - mPoint.top, mSize.width, mSize.height); } MyGUI::ResourceImageSetPtr ResourceImageSetPointerFix::getImageSet() { return mImageSet; } MyGUI::IntPoint ResourceImageSetPointerFix::getHotSpot() { return mPoint; } MyGUI::IntSize ResourceImageSetPointerFix::getSize() { return mSize; } } openmw-openmw-0.49.0/apps/openmw/mwgui/cursor.hpp000066400000000000000000000025561503074453300220560ustar00rootroot00000000000000#ifndef MWGUI_CURSOR_H #define MWGUI_CURSOR_H #include #include namespace MWGui { /// \brief Allows us to get the members of /// ResourceImageSetPointer that we need. /// \example MyGUI::FactoryManager::getInstance().registerFactory("Resource", /// "ResourceImageSetPointer"); /// MyGUI::ResourceManager::getInstance().load("core.xml"); class ResourceImageSetPointerFix final : public MyGUI::IPointer { MYGUI_RTTI_DERIVED(ResourceImageSetPointerFix) public: ResourceImageSetPointerFix(); virtual ~ResourceImageSetPointerFix(); void deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) override; 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. MyGUI::ResourceImageSetPtr getImageSet(); MyGUI::IntPoint getHotSpot(); MyGUI::IntSize getSize(); int getRotation(); private: MyGUI::IntPoint mPoint; MyGUI::IntSize mSize; MyGUI::ResourceImageSetPtr mImageSet; int mRotation; // rotation in degrees }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/debugwindow.cpp000066400000000000000000000224161503074453300230470ustar00rootroot00000000000000#include "debugwindow.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include #ifndef BT_NO_PROFILE namespace { void bulletDumpRecursive(CProfileIterator* pit, int spacing, std::stringstream& os) { pit->First(); 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; int frames_since_reset = CProfileManager::Get_Frame_Count_Since_Reset(); for (i = 0; i < spacing; i++) os << "."; os << "----------------------------------\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; int numChildren = 0; 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; j < spacing; j++) os << "."; double ms = (current_total_time / (double)frames_since_reset); 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 } if (parent_time < accumulated_time) { os << "what's wrong\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; i < numChildren; i++) { pit->Enter_Child(i); bulletDumpRecursive(pit, spacing + 3, os); pit->Enter_Parent(); } } void bulletDumpAll(std::stringstream& os) { CProfileIterator* profileIterator = 0; profileIterator = CProfileManager::Get_Iterator(); bulletDumpRecursive(profileIterator, 0, os); CProfileManager::Release_Iterator(profileIterator); } } #endif // BT_NO_PROFILE namespace MWGui { DebugWindow::DebugWindow() : WindowBase("openmw_debug_window.layout") { getWidget(mTabControl, "TabControl"); // Ideas for other tabs: // - Texture / compositor texture viewer // - Material editor // - Shader editor 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::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::updateLogView() { 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 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); } void DebugWindow::updateLuaProfile() { if (mLuaProfiler->isTextSelection()) return; 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); if (mBulletProfilerEdit->isTextSelection()) // pause updating while user is trying to copy text return; size_t previousPos = mBulletProfilerEdit->getVScrollPosition(); mBulletProfilerEdit->setCaption(stream.str()); 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:; } } } openmw-openmw-0.49.0/apps/openmw/mwgui/debugwindow.hpp000066400000000000000000000010751503074453300230520ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_DEBUGWINDOW_H #define OPENMW_MWGUI_DEBUGWINDOW_H #include "windowbase.hpp" namespace MWGui { class DebugWindow : public WindowBase { public: DebugWindow(); void onFrame(float dt) override; static void startLogRecording(); private: void updateLogView(); void updateLuaProfile(); void updateBulletProfile(); MyGUI::TabControl* mTabControl; MyGUI::EditBox* mLogView; MyGUI::EditBox* mLuaProfiler; MyGUI::EditBox* mBulletProfilerEdit; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/dialogue.cpp000066400000000000000000001023151503074453300223170ustar00rootroot00000000000000#include "dialogue.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.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 "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "bookpage.hpp" #include "textcolours.hpp" #include "journalbooks.hpp" // to_utf8_span namespace MWGui { void ResponseCallback::addResponse(std::string_view title, std::string_view text) { mWindow->addResponse(title, text, mNeedMargin); } void ResponseCallback::updateTopics() const { mWindow->updateTopics(); } PersuasionDialog::PersuasionDialog(std::unique_ptr callback) : WindowModal("openmw_persuasion_dialog.layout") , mCallback(std::move(callback)) , mInitialGoldLabelWidth(0) , mInitialMainWidgetWidth(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mAdmireButton, "AdmireButton"); getWidget(mIntimidateButton, "IntimidateButton"); getWidget(mTauntButton, "TauntButton"); getWidget(mBribe10Button, "Bribe10Button"); 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); mIntimidateButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mTauntButton->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe10Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe100Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); mBribe1000Button->eventMouseButtonClick += MyGUI::newDelegate(this, &PersuasionDialog::onPersuade); } 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) { 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; else if (sender == mBribe10Button) type = MWBase::MechanicsManager::PT_Bribe10; else if (sender == mBribe100Button) type = MWBase::MechanicsManager::PT_Bribe100; else /*if (sender == mBribe1000Button)*/ type = MWBase::MechanicsManager::PT_Bribe1000; MWBase::Environment::get().getDialogueManager()->persuade(type, mCallback.get()); mCallback->updateTopics(); setVisible(false); } void PersuasionDialog::onOpen() { center(); 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); 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(); } MyGUI::Widget* PersuasionDialog::getDefaultKeyFocus() { return mAdmireButton; } // -------------------------------------------------------------------------------------------------- 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 { typesetter->sectionBreak(mNeedMargin ? 9 : 0); auto windowManager = MWBase::Environment::get().getWindowManager(); if (!mTitle.empty()) { const MyGUI::Colour& headerColour = windowManager->getTextColours().header; BookTypesetter::Style* title = typesetter->createStyle({}, headerColour, false); typesetter->write(title, to_utf8_span(mTitle)); typesetter->sectionBreak(); } typedef std::pair Range; std::map hyperLinks; // We need this copy for when @# hyperlinks are replaced std::string text = mText; size_t pos_end = std::string::npos; for (;;) { size_t pos_begin = text.find('@'); if (pos_begin != std::string::npos) pos_end = text.find('#', pos_begin); if (pos_begin != std::string::npos && pos_end != std::string::npos) { 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 = Misc::StringUtils::lowerCase(windowManager->getTranslationDataStorage().topicStandardForm(link)); 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); 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)); 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); 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); if (formatted < hyperLink.first.first) typesetter->write(style, formatted, hyperLink.first.first); typesetter->write(hotStyle, hyperLink.first.first, hyperLink.first.second); formatted = hyperLink.first.second; } if (formatted < text.size()) typesetter->write(style, formatted, text.size()); } else { std::vector matches; keywordSearch->highlightKeywords(text.begin(), text.end(), matches); 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, match.mValue, match.mBeg - text.begin(), match.mEnd - text.begin()); i = match.mEnd; } if (i != text.end()) addTopicLink(std::move(typesetter), 0, i - text.begin(), text.size()); } } void Response::addTopicLink(BookTypesetter::Ptr typesetter, intptr_t topicId, size_t begin, size_t end) const { const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); 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); } Message::Message(std::string_view text) { mText = text; } 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); typesetter->sectionBreak(9); typesetter->write(title, to_utf8_span(mText)); } // -------------------------------------------------------------------------------------------------- void Choice::activated() { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventChoiceActivated(mChoiceId); } void Topic::activated() { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventTopicActivated(mTopicId); } void Goodbye::activated() { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); eventActivated(); } // -------------------------------------------------------------------------------------------------- DialogueWindow::DialogueWindow() : WindowBase("openmw_dialogue_window.layout") , mIsCompanion(false) , mGoodbye(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 getWidget(mHistory, "History"); // Topics list getWidget(mTopicsList, "TopicsList"); mTopicsList->eventItemSelected += MyGUI::newDelegate(this, &DialogueWindow::onSelectListItem); getWidget(mGoodbyeButton, "ByeButton"); mGoodbyeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &DialogueWindow::onByeClicked); getWidget(mDispositionBar, "Disposition"); getWidget(mDispositionText, "DispositionText"); getWidget(mScrollBar, "VScroll"); mScrollBar->eventScrollChangePosition += MyGUI::newDelegate(this, &DialogueWindow::onScrollbarMoved); mHistory->eventMouseWheel += MyGUI::newDelegate(this, &DialogueWindow::onMouseWheel); BookPage::ClickCallback callback = [this](TypesetBook::InteractiveId link) { notifyLinkClicked(link); }; mHistory->adviseLinkClicked(std::move(callback)); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &DialogueWindow::onWindowResize); } void DialogueWindow::onTradeComplete() { MyGUI::UString message = MyGUI::LanguageManager::getInstance().replaceTags("#{sBarterDialog5}"); addResponse({}, message); } bool DialogueWindow::exit() { if ((MWBase::Environment::get().getDialogueManager()->isInChoice())) { return false; } else { resetReference(); MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); mTopicsList->scrollToTop(); return true; } } 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; redrawTopicsList(); updateHistory(); mCurrentWindowSize = _sender->getSize(); } void DialogueWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { if (!mScrollBar->getVisible()) return; mScrollBar->setScrollPosition( std::clamp(mScrollBar->getScrollPosition() - _rel * 0.3, 0, mScrollBar->getScrollRange() - 1)); onScrollbarMoved(mScrollBar, mScrollBar->getScrollPosition()); } void DialogueWindow::onByeClicked(MyGUI::Widget* _sender) { if (exit()) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } } void DialogueWindow::onSelectListItem(const std::string& topic, int id) { MWBase::DialogueManager* dialogueManager = MWBase::Environment::get().getDialogueManager(); if (mGoodbye || dialogueManager->isInChoice()) return; 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()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); } else if (topic == sPersuasion) mPersuasionDialog.setVisible(true); else if (topic == sCompanionShare) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Companion, mPtr); else if (!dialogueManager->checkServiceRefused(mCallback.get())) { 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)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellBuying, mPtr); 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)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_SpellCreation, mPtr); 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)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Training, mPtr); else if (topic == sRepair && !dialogueManager->checkServiceRefused(mCallback.get(), MWBase::DialogueManager::Repair)) MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_MerchantRepair, mPtr); } else updateTopics(); } void DialogueWindow::setPtr(const MWWorld::Ptr& actor) { if (actor.isEmpty() || !actor.getClass().isActor()) { Log(Debug::Warning) << "Warning: can not talk with non-actor object."; return; } bool sameActor = (mPtr == actor); if (!sameActor) { // The history is not reset here mKeywords.clear(); mTopicsList->clear(); for (auto& link : mLinks) mDeleteLater.push_back( std::move(link)); // Links are not deleted right away to prevent issues with event handlers mLinks.clear(); } mPtr = actor; mGoodbye = false; mTopicsList->setEnabled(true); if (!MWBase::Environment::get().getDialogueManager()->startDialogue(actor, mGreetingCallback.get())) { // No greetings found. The dialogue window should not be shown. // If this is a companion, we must show the companion window directly (used by BM_bear_be_unique). MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); mPtr = MWWorld::Ptr(); if (isCompanion(actor)) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Companion, actor); return; } MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); setTitle(mPtr.getClass().getName(mPtr)); updateTopics(); updateTopicsPane(); // force update for new services updateDisposition(); restock(); } void DialogueWindow::restock() { 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) { sellerStats.setGoldPool(mPtr.getClass().getBaseGold(mPtr)); sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); } } void DialogueWindow::deleteLater() { mDeleteLater.clear(); } void DialogueWindow::onClose() { if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Dialogue)) return; // Reset history mHistoryContents.clear(); } bool DialogueWindow::setKeywords(const std::list& keyWords) { if (mKeywords == keyWords && isCompanion() == mIsCompanion) return false; mIsCompanion = isCompanion(); mKeywords = keyWords; updateTopicsPane(); 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(std::move(linkPair.second)); mTopicLinks.clear(); mKeywordSearch.clear(); int services = mPtr.getClass().getServices(mPtr); 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().getESMStore()->get(); if (mPtr.getType() == ESM::NPC::sRecordId) mTopicsList->addItem(gmst.find("sPersuasion")->mValue.getString()); if (services & ESM::NPC::AllItems) mTopicsList->addItem(gmst.find("sBarter")->mValue.getString()); if (services & ESM::NPC::Spells) mTopicsList->addItem(gmst.find("sSpells")->mValue.getString()); if (travel) mTopicsList->addItem(gmst.find("sTravel")->mValue.getString()); if (services & ESM::NPC::Spellmaking) mTopicsList->addItem(gmst.find("sSpellmakingMenuTitle")->mValue.getString()); if (services & ESM::NPC::Enchanting) mTopicsList->addItem(gmst.find("sEnchanting")->mValue.getString()); if (services & ESM::NPC::Training) mTopicsList->addItem(gmst.find("sServiceTrainingTitle")->mValue.getString()); if (services & ESM::NPC::Repair) mTopicsList->addItem(gmst.find("sRepair")->mValue.getString()); if (isCompanion()) mTopicsList->addItem(gmst.find("sCompanionShare")->mValue.getString()); if (mTopicsList->getItemCount() > 0) mTopicsList->addSeparator(); for (const auto& keyword : mKeywords) { std::string topicId = Misc::StringUtils::lowerCase(keyword); mTopicsList->addItem(keyword); auto t = std::make_unique(keyword); mKeywordSearch.seed(topicId, intptr_t(t.get())); t->eventTopicActivated += MyGUI::newDelegate(this, &DialogueWindow::onTopicActivated); mTopicLinks[topicId] = std::move(t); } redrawTopicsList(); updateHistory(); } void DialogueWindow::updateHistory(bool scrollbar) { if (!scrollbar && mScrollBar->getVisible()) { 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)); mScrollBar->setVisible(true); } BookTypesetter::Ptr typesetter = BookTypesetter::create(mHistory->getWidth(), std::numeric_limits::max()); for (const auto& text : mHistoryContents) text->write(typesetter, &mKeywordSearch, mTopicLinks); BookTypesetter::Style* body = typesetter->createStyle({}, MyGUI::Colour::White, false); typesetter->sectionBreak(9); // choices const TextColours& textColours = MWBase::Environment::get().getWindowManager()->getTextColours(); mChoices = MWBase::Environment::get().getDialogueManager()->getChoices(); for (std::pair& choice : mChoices) { auto link = std::make_unique(choice.second); link->eventChoiceActivated += MyGUI::newDelegate(this, &DialogueWindow::onChoiceActivated); 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, interactiveId); typesetter->write(questionStyle, to_utf8_span(choice.first)); } mGoodbye = MWBase::Environment::get().getDialogueManager()->isGoodbye(); if (mGoodbye) { auto link = std::make_unique(); link->eventActivated += MyGUI::newDelegate(this, &DialogueWindow::onGoodbyeActivated); 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)); } TypesetBook::Ptr book = typesetter->complete(); mHistory->showPage(book, 0); size_t viewHeight = mHistory->getParent()->getHeight(); if (!scrollbar && book->getSize().second > viewHeight) updateHistory(true); else if (scrollbar) { mHistory->setSize(MyGUI::IntSize(mHistory->getWidth(), book->getSize().second)); // Scroll range should be >= 2 to enable scrolling and prevent a crash size_t range = std::max(book->getSize().second - viewHeight, size_t(2)); mScrollBar->setScrollRange(range); mScrollBar->setScrollPosition(range - 1); mScrollBar->setTrackSize( static_cast(viewHeight / static_cast(book->getSize().second) * mScrollBar->getLineSize())); onScrollbarMoved(mScrollBar, range - 1); } else { // no scrollbar onScrollbarMoved(mScrollBar, 0); } bool goodbyeEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() || mGoodbye; bool goodbyeWasEnabled = mGoodbyeButton->getEnabled(); mGoodbyeButton->setEnabled(goodbyeEnabled); if (goodbyeEnabled && !goodbyeWasEnabled) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mGoodbyeButton); bool topicsEnabled = !MWBase::Environment::get().getDialogueManager()->isInChoice() && !mGoodbye; mTopicsList->setEnabled(topicsEnabled); } void DialogueWindow::notifyLinkClicked(TypesetBook::InteractiveId link) { reinterpret_cast(link)->activated(); } void DialogueWindow::onTopicActivated(const std::string& topicId) { if (mGoodbye) return; MWBase::Environment::get().getDialogueManager()->keywordSelected(topicId, mCallback.get()); updateTopics(); } void DialogueWindow::onChoiceActivated(int id) { if (mGoodbye) { onGoodbyeActivated(); return; } MWBase::Environment::get().getDialogueManager()->questionAnswered(id, mCallback.get()); updateTopics(); } void DialogueWindow::onGoodbyeActivated() { MWBase::Environment::get().getDialogueManager()->goodbyeSelected(); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); resetReference(); } void DialogueWindow::onScrollbarMoved(MyGUI::ScrollBar* sender, size_t pos) { mHistory->setPosition(0, static_cast(pos) * -1); } void DialogueWindow::addResponse(std::string_view title, std::string_view text, bool needMargin) { mHistoryContents.push_back(std::make_unique(text, title, needMargin)); updateHistory(); } void DialogueWindow::addMessageBox(std::string_view text) { mHistoryContents.push_back(std::make_unique(text)); updateHistory(); } void DialogueWindow::updateDisposition() { 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")); } if (mDispositionBar->getVisible() != dispositionVisible) { mDispositionBar->setVisible(dispositionVisible); const int offset = (mDispositionBar->getHeight() + 5) * (dispositionVisible ? 1 : -1); mTopicsList->setCoord(mTopicsList->getCoord() + MyGUI::IntCoord(0, offset, 0, -offset)); redrawTopicsList(); } } void DialogueWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); } void DialogueWindow::onFrame(float dt) { checkReferenceAvailable(); if (mPtr.isEmpty()) return; updateDisposition(); deleteLater(); if (mChoices != MWBase::Environment::get().getDialogueManager()->getChoices() || mGoodbye != MWBase::Environment::get().getDialogueManager()->isGoodbye()) updateHistory(); } void DialogueWindow::updateTopicFormat() { if (!Settings::gui().mColorTopicEnable) return; for (const std::string& keyword : mKeywords) { int flag = MWBase::Environment::get().getDialogueManager()->getTopicFlag(ESM::RefId::stringRefId(keyword)); MyGUI::Button* button = mTopicsList->getItemWidget(keyword); const auto oldCaption = button->getCaption(); const MyGUI::IntSize oldSize = button->getSize(); bool changed = false; if (flag & MWBase::DialogueManager::TopicType::Specific) { button->changeWidgetSkin("MW_ListLine_Specific"); changed = true; } else if (flag & MWBase::DialogueManager::TopicType::Exhausted) { button->changeWidgetSkin("MW_ListLine_Exhausted"); changed = true; } if (changed) { button->setCaption(oldCaption); button->setTextAlign(MyGUI::Align::Left); MyGUI::ISubWidgetText* text = button->getSubWidgetText(); if (text != nullptr) text->setWordWrap(true); button->setSize(oldSize); } } } void DialogueWindow::updateTopics() { // Topic formatting needs to be updated regardless of whether the topic list has changed if (!setKeywords(MWBase::Environment::get().getDialogueManager()->getAvailableTopics())) updateTopicFormat(); } bool DialogueWindow::isCompanion() { return isCompanion(mPtr); } bool DialogueWindow::isCompanion(const MWWorld::Ptr& actor) { if (actor.isEmpty()) return false; return !actor.getClass().getScript(actor).empty() && actor.getRefData().getLocals().getIntVar(actor.getClass().getScript(actor), "companion"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/dialogue.hpp000066400000000000000000000142541503074453300223300ustar00rootroot00000000000000#ifndef MWGUI_DIALOGE_H #define MWGUI_DIALOGE_H #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 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(std::unique_ptr callback); void onOpen() override; MyGUI::Widget* getDefaultKeyFocus() override; private: std::unique_ptr mCallback; int mInitialGoldLabelWidth; int mInitialMainWidgetWidth; MyGUI::Button* mCancelButton; MyGUI::Button* mAdmireButton; MyGUI::Button* mIntimidateButton; MyGUI::Button* mTauntButton; MyGUI::Button* mBribe10Button; MyGUI::Button* mBribe100Button; MyGUI::Button* mBribe1000Button; MyGUI::Widget* mActionsBox; Gui::AutoSizedTextBox* mGoldLabel; 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; }; struct Topic : Link { typedef MyGUI::delegates::MultiDelegate EventHandle_TopicId; EventHandle_TopicId eventTopicActivated; Topic(const std::string& id) : mTopicId(id) { } std::string mTopicId; void activated() override; }; struct Choice : Link { typedef MyGUI::delegates::MultiDelegate EventHandle_ChoiceId; EventHandle_ChoiceId eventChoiceActivated; Choice(int id) : mChoiceId(id) { } int mChoiceId; void activated() override; }; struct Goodbye : Link { typedef MyGUI::delegates::MultiDelegate<> Event_Activated; Event_Activated eventActivated; void activated() override; }; typedef MWDialogue::KeywordSearch KeywordSearchT; struct DialogueText { virtual ~DialogueText() = default; virtual void write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map>& topicLinks) const = 0; std::string mText; }; struct Response : DialogueText { 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(std::string_view text); void write(BookTypesetter::Ptr typesetter, KeywordSearchT* keywordSearch, std::map>& topicLinks) const override; }; class DialogueWindow : public WindowBase, public ReferenceInterface { public: DialogueWindow(); void onTradeComplete(); bool exit() override; // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; void notifyLinkClicked(TypesetBook::InteractiveId link); void setPtr(const MWWorld::Ptr& actor) override; /// @return true if stale keywords were updated successfully bool setKeywords(const std::list& keyWord); void addResponse(std::string_view title, std::string_view text, bool needMargin = true); void addMessageBox(std::string_view text); void onFrame(float dt) override; void clear() override { resetReference(); } void updateTopics(); void onClose() override; std::string_view getWindowIdForLua() const override { return "Dialogue"; } protected: void updateTopicsPane(); bool isCompanion(const MWWorld::Ptr& actor); bool isCompanion(); void onSelectListItem(const std::string& topic, int id); void onByeClicked(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); void onWindowResize(MyGUI::Window* _sender); void onTopicActivated(const std::string& topicId); void onChoiceActivated(int id); void onGoodbyeActivated(); void onScrollbarMoved(MyGUI::ScrollBar* sender, size_t pos); void updateHistory(bool scrollbar = false); void onReferenceUnavailable() override; private: void updateDisposition(); void restock(); void deleteLater(); void redrawTopicsList(); bool mIsCompanion; std::list mKeywords; std::vector> mHistoryContents; std::vector> mChoices; bool mGoodbye; std::vector> mLinks; std::map> mTopicLinks; std::vector> mDeleteLater; KeywordSearchT mKeywordSearch; BookPage* mHistory; Gui::MWList* mTopicsList; MyGUI::ScrollBar* mScrollBar; MyGUI::ProgressBar* mDispositionBar; MyGUI::TextBox* mDispositionText; MyGUI::Button* mGoodbyeButton; PersuasionDialog mPersuasionDialog; MyGUI::IntSize mCurrentWindowSize; std::unique_ptr mCallback; std::unique_ptr mGreetingCallback; void updateTopicFormat(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/draganddrop.cpp000066400000000000000000000117101503074453300230110ustar00rootroot00000000000000#include "draganddrop.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "controllers.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { 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) { MWWorld::Ptr item = mSourceModel->moveItem(mItem, mDraggedCount, playerModel); playerModel->update(); ItemModel::ModelIndex newIndex = -1; for (size_t i = 0; i < playerModel->getItemCount(); ++i) { 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; } const ESM::RefId& sound = mItem.mBase.getClass().getUpSoundId(mItem.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); 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"); 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); sourceView->update(); MWBase::Environment::get().getWindowManager()->setDragDrop(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); // 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); } mSourceModel->update(); finish(); if (targetView) targetView->update(); MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); // 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::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); } } openmw-openmw-0.49.0/apps/openmw/mwgui/draganddrop.hpp000066400000000000000000000014051503074453300230160ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_DRAGANDDROP_H #define OPENMW_MWGUI_DRAGANDDROP_H #include "itemmodel.hpp" namespace MyGUI { class Widget; } namespace MWGui { class ItemView; class SortFilterItemModel; class DragAndDrop { public: bool mIsOnDragAndDrop; MyGUI::Widget* mDraggedWidget; ItemModel* mSourceModel; ItemView* mSourceView; SortFilterItemModel* mSourceSortModel; ItemStack mItem; int mDraggedCount; DragAndDrop(); void startDrag( int index, SortFilterItemModel* sortModel, ItemModel* sourceModel, ItemView* sourceView, int count); void drop(ItemModel* targetModel, ItemView* targetView); void onFrame(); void finish(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/enchantingdialog.cpp000066400000000000000000000337651503074453300240400ustar00rootroot00000000000000#include "enchantingdialog.hpp" #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { EnchantingDialog::EnchantingDialog() : WindowBase("openmw_enchanting_dialog.layout") , EffectEditorBase(EffectEditorBase::Enchanting) { getWidget(mName, "NameEdit"); getWidget(mCancelButton, "CancelButton"); getWidget(mAvailableEffectsList, "AvailableEffects"); getWidget(mUsedEffectsView, "UsedEffects"); getWidget(mItemBox, "ItemBox"); getWidget(mSoulBox, "SoulBox"); getWidget(mEnchantmentPoints, "Enchantment"); getWidget(mCastCost, "CastCost"); getWidget(mCharge, "Charge"); getWidget(mSuccessChance, "SuccessChance"); getWidget(mChanceLayout, "ChanceLayout"); getWidget(mTypeButton, "TypeButton"); getWidget(mBuyButton, "BuyButton"); getWidget(mPrice, "PriceLabel"); getWidget(mPriceText, "PriceTextLabel"); setWidgets(mAvailableEffectsList, mUsedEffectsView); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onCancelButtonClicked); mItemBox->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onSelectItem); mSoulBox->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onSelectSoul); mBuyButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onBuyButtonClicked); mTypeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EnchantingDialog::onTypeButtonClicked); mName->eventEditSelectAccept += MyGUI::newDelegate(this, &EnchantingDialog::onAccept); } void EnchantingDialog::onOpen() { center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mName); } void EnchantingDialog::setSoulGem(const MWWorld::Ptr& gem) { if (gem.isEmpty()) { mSoulBox->setItem(MWWorld::Ptr()); mSoulBox->clearUserStrings(); mEnchanting.setSoulGem(MWWorld::Ptr()); } else { mSoulBox->setItem(gem); mSoulBox->setUserString("ToolTipType", "ItemPtr"); mSoulBox->setUserData(MWWorld::Ptr(gem)); mEnchanting.setSoulGem(gem); } } void EnchantingDialog::setItem(const MWWorld::Ptr& item) { if (item.isEmpty()) { mItemBox->setItem(MWWorld::Ptr()); mItemBox->clearUserStrings(); mEnchanting.setOldItem(MWWorld::Ptr()); } else { std::string_view name = item.getClass().getName(item); mName->setCaption(MyGUI::UString(name)); mItemBox->setItem(item); mItemBox->setUserString("ToolTipType", "ItemPtr"); mItemBox->setUserData(MWWorld::Ptr(item)); mEnchanting.setOldItem(item); } } void EnchantingDialog::updateLabels() { 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::clamp(mEnchanting.getEnchantChance(), 0, 100))); mCastCost->setCaption(std::to_string(mEnchanting.getEffectiveCastCost())); mPrice->setCaption(std::to_string(mEnchanting.getEnchantPrice())); switch (mEnchanting.getCastStyle()) { case ESM::Enchantment::CastOnce: mTypeButton->setCaption(MyGUI::UString( MWBase::Environment::get().getWindowManager()->getGameSettingString("sItemCastOnce", "Cast Once"))); setConstantEffect(false); break; case ESM::Enchantment::WhenStrikes: mTypeButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( "sItemCastWhenStrikes", "When Strikes"))); setConstantEffect(false); break; case ESM::Enchantment::WhenUsed: mTypeButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( "sItemCastWhenUsed", "When Used"))); setConstantEffect(false); break; case ESM::Enchantment::ConstantEffect: mTypeButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString( "sItemCastConstant", "Cast Constant"))); setConstantEffect(true); break; } } void EnchantingDialog::setPtr(const MWWorld::Ptr& ptr) { 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()) { mEnchanting.setSelfEnchanting(false); mEnchanting.setEnchanter(ptr); mBuyButton->setCaptionWithReplacing("#{sBuy}"); mChanceLayout->setVisible(false); mPtr = ptr; setSoulGem(MWWorld::Ptr()); mPrice->setVisible(true); mPriceText->setVisible(true); } else { mEnchanting.setSelfEnchanting(true); mEnchanting.setEnchanter(MWMechanics::getPlayer()); mBuyButton->setCaptionWithReplacing("#{sCreate}"); mChanceLayout->setVisible(Settings::game().mShowEnchantChance); mPtr = MWMechanics::getPlayer(); setSoulGem(ptr); mPrice->setVisible(false); mPriceText->setVisible(false); } setItem(MWWorld::Ptr()); startEditing(); updateLabels(); } void EnchantingDialog::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); resetReference(); } void EnchantingDialog::resetReference() { ReferenceInterface::resetReference(); setItem(MWWorld::Ptr()); setSoulGem(MWWorld::Ptr()); mPtr = MWWorld::Ptr(); mEnchanting.setEnchanter(MWWorld::Ptr()); } void EnchantingDialog::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Enchanting); } void EnchantingDialog::onSelectItem(MyGUI::Widget* sender) { if (mEnchanting.getOldItem().isEmpty()) { mItemSelectionDialog = std::make_unique("#{sEnchantItems}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &EnchantingDialog::onItemSelected); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &EnchantingDialog::onItemCancel); mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyEnchantable); } else { setItem(MWWorld::Ptr()); updateLabels(); } } void EnchantingDialog::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); setItem(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); mEnchanting.nextCastStyle(); updateLabels(); } void EnchantingDialog::onItemCancel() { mItemSelectionDialog->setVisible(false); } void EnchantingDialog::onSoulSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); mEnchanting.setSoulGem(item); if (mEnchanting.getGemCharge() == 0) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage32}"); return; } setSoulGem(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateLabels(); } void EnchantingDialog::onSoulCancel() { mItemSelectionDialog->setVisible(false); } void EnchantingDialog::onSelectSoul(MyGUI::Widget* sender) { if (mEnchanting.getGem().isEmpty()) { 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}"); } else { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } } void EnchantingDialog::notifyEffectsChanged() { mEffectList.populate(mEffects); mEnchanting.setEffect(mEffectList); updateLabels(); } void EnchantingDialog::onTypeButtonClicked(MyGUI::Widget* sender) { mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } void EnchantingDialog::onAccept(MyGUI::EditBox* sender) { onBuyButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void EnchantingDialog::onBuyButtonClicked(MyGUI::Widget* sender) { if (mEffects.size() <= 0) { MWBase::Environment::get().getWindowManager()->messageBox("#{sEnchantmentMenu11}"); return; } if (mName->getCaption().empty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage10}"); return; } if (mEnchanting.soulEmpty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage52}"); return; } if (mEnchanting.itemEmpty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage11}"); return; } if (static_cast(mEnchanting.getEnchantPoints(false)) > mEnchanting.getMaxEnchantValue()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage29}"); return; } mEnchanting.setNewItemName(mName->getCaption()); mEnchanting.setEffect(mEffectList); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (mPtr != player && mEnchanting.getEnchantPrice() > playerGold) { 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) { MWWorld::Ptr item = (i == 0) ? mEnchanting.getOldItem() : mEnchanting.getGem(); if (MWBase::Environment::get().getMechanicsManager()->isItemStolenFrom( item.getCellRef().getRefId(), mPtr)) { 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().getWindowManager()->removeGuiMode(GM_Enchanting); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return; } } } if (mEnchanting.create()) { 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(ESM::RefId::stringRefId("enchant fail")); MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage34}"); if (!mEnchanting.getGem().isEmpty() && !mEnchanting.getGem().getCellRef().getCount()) { setSoulGem(MWWorld::Ptr()); mEnchanting.nextCastStyle(); updateLabels(); updateEffectsView(); } } } } openmw-openmw-0.49.0/apps/openmw/mwgui/enchantingdialog.hpp000066400000000000000000000042511503074453300240310ustar00rootroot00000000000000#ifndef MWGUI_ENCHANTINGDIALOG_H #define MWGUI_ENCHANTINGDIALOG_H #include #include "itemselection.hpp" #include "spellcreationdialog.hpp" #include "../mwmechanics/enchanting.hpp" namespace MWGui { class ItemWidget; class EnchantingDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { public: 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); /// Actor Ptr: buy enchantment from this actor /// Soulgem Ptr: player self-enchant void setPtr(const MWWorld::Ptr& ptr) override; 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 onItemSelected(MWWorld::Ptr item); void onItemCancel(); void onSoulSelected(MWWorld::Ptr item); void onSoulCancel(); void onBuyButtonClicked(MyGUI::Widget* sender); void updateLabels(); void onTypeButtonClicked(MyGUI::Widget* sender); void onAccept(MyGUI::EditBox* sender); std::unique_ptr mItemSelectionDialog; MyGUI::Widget* mChanceLayout; MyGUI::Button* mCancelButton; ItemWidget* mItemBox; ItemWidget* mSoulBox; MyGUI::Button* mTypeButton; MyGUI::Button* mBuyButton; MyGUI::EditBox* mName; MyGUI::TextBox* mEnchantmentPoints; MyGUI::TextBox* mCastCost; MyGUI::TextBox* mCharge; MyGUI::TextBox* mSuccessChance; MyGUI::TextBox* mPrice; MyGUI::TextBox* mPriceText; MWMechanics::Enchanting mEnchanting; ESM::EffectList mEffectList; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/exposedwindow.cpp000066400000000000000000000011601503074453300234210ustar00rootroot00000000000000#include "exposedwindow.hpp" namespace MWGui { MyGUI::VectorWidgetPtr Window::getSkinWidgetsByName(const std::string& name) { return MyGUI::Widget::getSkinWidgetsByName(name); } MyGUI::Widget* Window::getSkinWidget(const std::string& _name, bool _throw) { MyGUI::VectorWidgetPtr widgets = getSkinWidgetsByName(_name); if (widgets.empty()) { MYGUI_ASSERT(!_throw, "widget name '" << _name << "' not found in skin of layout '" << getName() << "'"); return nullptr; } else { return widgets[0]; } } } openmw-openmw-0.49.0/apps/openmw/mwgui/exposedwindow.hpp000066400000000000000000000010251503074453300234260ustar00rootroot00000000000000#ifndef MWGUI_EXPOSEDWINDOW_H #define MWGUI_EXPOSEDWINDOW_H #include namespace MWGui { /** * @brief subclass to provide access to some Widget internals. */ class Window : public MyGUI::Window { MYGUI_RTTI_DERIVED(Window) public: MyGUI::VectorWidgetPtr getSkinWidgetsByName(const std::string& name); MyGUI::Widget* getSkinWidget(const std::string& _name, bool _throw = true); ///< Get a widget defined in the inner skin of this window. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/formatting.cpp000066400000000000000000000460711503074453300227060ustar00rootroot00000000000000#include "formatting.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwscript/interpretercontext.hpp" namespace MWGui::Formatting { /* BookTextParser */ BookTextParser::BookTextParser(const std::string& text, bool shrinkTextAtLastTag) : 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); 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("
"); size_t pIndex = lowerText.rfind("

"); mPlainTextEnd = 0; if (brIndex != pIndex) { if (brIndex != std::string::npos && pIndex != std::string::npos) mPlainTextEnd = std::max(brIndex, pIndex); else if (brIndex != std::string::npos) mPlainTextEnd = brIndex; else mPlainTextEnd = pIndex; } } 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; } std::string BookTextParser::getReadyText() const { return mReadyText; } BookTextParser::Events BookTextParser::next() { while (mIndex < mText.size()) { 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)) { 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 (!mIgnoreNewlineTags) { if (type == Event_BrTag) mBuffer.push_back('\n'); else { mBuffer.append("\n\n"); } } mIgnoreLineEndings = true; if (type == Event_PTag && !mAttributes.empty()) flushBuffer(); } else flushBuffer(); if (type == Event_ImgTag) { mIgnoreNewlineTags = false; } ++mIndex; return type; } } else { if (!mIgnoreLineEndings || ch != '\n') { if (mIndex < mPlainTextEnd) mBuffer.push_back(ch); mIgnoreLineEndings = false; mIgnoreNewlineTags = false; } } ++mIndex; } flushBuffer(); return Event_EOF; } void BookTextParser::flushBuffer() { mReadyText = mBuffer; mBuffer.clear(); } 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) { mTag.erase(mTag.begin()); return; } if (tagNameEndPos == std::string::npos) return; tag.erase(0, tagNameEndPos + 1); while (!tag.empty()) { 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] == '"' || 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) { value = tag; tag.erase(); } else { value = tag.substr(0, valEndPos); tag.erase(0, valEndPos + 1); } } mAttributes[key] = std::move(value); } } /* 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)); } 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); BookTextParser parser(markup, shrinkTextAtLastTag); 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(); // 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()) { if (plainText[0] == '\n') plainText.erase(plainText.begin()); else { 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 (brAtStart && !brBeforeLastTag && !isPrevImg) plainText.erase(plainText.begin()); if (plainText.size() && brAtEnd) plainText.erase(plainText.end() - 1); if (!plainText.empty() || brBeforeLastTag || isPrevImg) { if (inlineImageInserted) { pag.setCurrentTop(pag.getCurrentTop() - mTextStyle.mTextSize); plainText = " " + plainText; inlineImageInserted = false; } TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); elem.paginate(); } brBeforeLastTag = brAtEnd; } if (event == BookTextParser::Event_EOF) break; isPrevImg = (event == BookTextParser::Event_ImgTag); switch (event) { case BookTextParser::Event_PageBreak: pag << Paginator::Page(pag.getStartTop(), pag.getCurrentTop()); pag.setStartTop(pag.getCurrentTop()); break; case BookTextParser::Event_ImgTag: { const BookTextParser::Attributes& attr = parser.getAttributes(); 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); const std::string& src = srcIt->second; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); std::string correctedSrc; constexpr std::string_view imgPrefix = "img://"; if (src.starts_with(imgPrefix)) { correctedSrc = src.substr(imgPrefix.size(), src.size() - imgPrefix.size()); if (width == 0) { width = 50; inlineImageInserted = true; } if (height == 0) height = 50; } else { if (width == 0 || height == 0) continue; correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); } if (!vfs->exists(correctedSrc)) { Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; break; } pag.setIgnoreLeadingEmptyLines(false); 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; } } // insert last page if (pag.getStartTop() != pag.getCurrentTop()) pag << Paginator::Page(pag.getStartTop(), pag.getStartTop() + pag.getPageHeight()); paper->setSize(paper->getWidth(), pag.getCurrentTop()); return pag.getPages(); } Paginator::Pages BookFormatter::markupToWidget( MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag) { return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight(), shrinkTextAtLastTag); } void BookFormatter::resetFontProperties() { mTextStyle = TextStyle(); } 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; } 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); 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()) { const std::string& face = it->second; std::string name{ Gui::FontLoader::getFontForFace(face) }; mTextStyle.mFont = "Journalbook " + name; } if (attr.find("size") != attr.end()) { /// \todo } } /* GraphicElement */ GraphicElement::GraphicElement(MyGUI::Widget* parent, Paginator& pag, const BlockStyle& blockStyle) : mParent(parent) , mPaginator(pag) , mBlockStyle(blockStyle) { } void GraphicElement::paginate() { int newTop = mPaginator.getCurrentTop() + getHeight(); while (newTop - mPaginator.getStartTop() > mPaginator.getPageHeight()) { int newStartTop = pageSplit(); mPaginator << Paginator::Page(mPaginator.getStartTop(), newStartTop); mPaginator.setStartTop(newStartTop); } 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) { if (lines[i].width == 0) ret += lineHeight; else { mPaginator.setIgnoreLeadingEmptyLines(false); break; } } 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"); } 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(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/formatting.hpp000066400000000000000000000123071503074453300227060ustar00rootroot00000000000000#ifndef MWGUI_FORMATTING_H #define MWGUI_FORMATTING_H #include #include #include namespace MWGui { namespace Formatting { struct TextStyle { TextStyle() : mColour(0, 0, 0) , mFont("Journalbook DefaultFont") , mTextSize(16) { } MyGUI::Colour mColour; std::string mFont; int mTextSize; }; struct BlockStyle { BlockStyle() : mAlign(MyGUI::Align::Left | MyGUI::Align::Top) { } MyGUI::Align mAlign; }; 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, 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; }; /// \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, bool shrinkTextAtLastTag); Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag); private: void resetFontProperties(); void handleDiv(const BookTextParser::Attributes& attr); void handleFont(const BookTextParser::Attributes& attr); 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; }; 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; }; 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; }; } } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/hud.cpp000066400000000000000000000600131503074453300213040ustar00rootroot00000000000000#include "hud.hpp" #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/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "draganddrop.hpp" #include "inventorywindow.hpp" #include "itemmodel.hpp" #include "spellicons.hpp" #include "itemwidget.hpp" namespace MWGui { /** * Makes it possible to use ItemModel::moveItem to move an item from an inventory to the world. */ class WorldItemModel : public ItemModel { public: WorldItemModel(float left, float top) : mLeft(left) , mTop(top) { } virtual ~WorldItemModel() override {} MWWorld::Ptr dropItemImpl(const ItemStack& item, size_t count, bool copy) { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr dropped; if (world->canPlaceObject(mLeft, mTop)) dropped = world->placeObject(item.mBase, mLeft, mTop, count, copy); else dropped = world->dropObjectOnGround(world->getPlayerPtr(), item.mBase, count, copy); dropped.getCellRef().setOwner(ESM::RefId()); return dropped; } MWWorld::Ptr addItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override { return dropItemImpl(item, count, false); } MWWorld::Ptr copyItem(const ItemStack& item, size_t count, bool /*allowAutoEquip*/) override { return dropItemImpl(item, count, true); } void removeItem(const ItemStack& item, size_t count) override { throw std::runtime_error("removeItem not implemented"); } ModelIndex getIndex(const ItemStack& item) override { throw std::runtime_error("getIndex not implemented"); } void update() override {} size_t getItemCount() override { return 0; } ItemStack getItem(ModelIndex index) override { throw std::runtime_error("getItem not implemented"); } bool usesContainer(const MWWorld::Ptr&) override { return false; } private: // Where to drop the item float mLeft; float mTop; }; HUD::HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender) : WindowBase("openmw_hud.layout") , LocalMapBase(customMarkers, localMapRender, Settings::map().mLocalMapHudFogOfWar) , mHealth(nullptr) , mMagicka(nullptr) , mStamina(nullptr) , mDrowning(nullptr) , mWeapImage(nullptr) , mSpellImage(nullptr) , mWeapStatus(nullptr) , mSpellStatus(nullptr) , mEffectBox(nullptr) , mMinimap(nullptr) , mCrosshair(nullptr) , mCellNameBox(nullptr) , mDrowningBar(nullptr) , mDrowningFlash(nullptr) , mHealthManaStaminaBaseLeft(0) , mWeapBoxBaseLeft(0) , mSpellBoxBaseLeft(0) , mMinimapBoxBaseRight(0) , mEffectBoxBaseRight(0) , mDragAndDrop(dragAndDrop) , mCellNameTimer(0.0f) , mWeaponSpellTimer(0.f) , mMapVisible(true) , mWeaponVisible(true) , mSpellVisible(true) , mWorldMouseOver(false) , mEnemyActorId(-1) , mEnemyHealthTimer(-1) , mIsDrowning(false) , mDrowningFlashTheta(0.f) { // Energy bars getWidget(mHealthFrame, "HealthFrame"); getWidget(mHealth, "Health"); getWidget(mMagicka, "Magicka"); getWidget(mStamina, "Stamina"); getWidget(mEnemyHealth, "EnemyHealth"); mHealthManaStaminaBaseLeft = mHealthFrame->getLeft(); MyGUI::Widget *healthFrame, *magickaFrame, *fatigueFrame; getWidget(healthFrame, "HealthFrame"); getWidget(magickaFrame, "MagickaFrame"); getWidget(fatigueFrame, "FatigueFrame"); healthFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); magickaFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); fatigueFrame->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onHMSClicked); // Drowning bar getWidget(mDrowningBar, "DrowningBar"); getWidget(mDrowningFrame, "DrowningFrame"); getWidget(mDrowning, "Drowning"); getWidget(mDrowningFlash, "Flash"); mDrowning->setProgressRange(200); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); // Item and spell images and status bars getWidget(mWeapBox, "WeapBox"); getWidget(mWeapImage, "WeapImage"); getWidget(mWeapStatus, "WeapStatus"); mWeapBoxBaseLeft = mWeapBox->getLeft(); mWeapBox->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWeaponClicked); getWidget(mSpellBox, "SpellBox"); getWidget(mSpellImage, "SpellImage"); getWidget(mSpellStatus, "SpellStatus"); mSpellBoxBaseLeft = mSpellBox->getLeft(); mSpellBox->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMagicClicked); getWidget(mSneakBox, "SneakBox"); mSneakBoxBaseLeft = mSneakBox->getLeft(); getWidget(mEffectBox, "EffectBox"); mEffectBoxBaseRight = viewSize.width - mEffectBox->getRight(); getWidget(mMinimapBox, "MiniMapBox"); mMinimapBoxBaseRight = viewSize.width - mMinimapBox->getRight(); getWidget(mMinimap, "MiniMap"); getWidget(mCompass, "Compass"); getWidget(mMinimapButton, "MiniMapButton"); mMinimapButton->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); getWidget(mCellNameBox, "CellName"); getWidget(mWeaponSpellBox, "WeaponSpellName"); getWidget(mCrosshair, "Crosshair"); LocalMapBase::init(mMinimap, mCompass); mMainWidget->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onWorldClicked); mMainWidget->eventMouseMove += MyGUI::newDelegate(this, &HUD::onWorldMouseOver); mMainWidget->eventMouseLostFocus += MyGUI::newDelegate(this, &HUD::onWorldMouseLostFocus); mSpellIcons = std::make_unique(); } HUD::~HUD() { mMainWidget->eventMouseLostFocus.clear(); mMainWidget->eventMouseMove.clear(); mMainWidget->eventMouseButtonClick.clear(); } 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); MyGUI::Widget* w; std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); if (id == "HBar") { mHealth->setProgressRange(std::max(0, modified)); mHealth->setProgressPosition(std::max(0, current)); getWidget(w, "HealthFrame"); w->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } else if (id == "MBar") { mMagicka->setProgressRange(std::max(0, modified)); mMagicka->setProgressPosition(std::max(0, current)); getWidget(w, "MagickaFrame"); w->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } else if (id == "FBar") { mStamina->setProgressRange(std::max(0, modified)); mStamina->setProgressPosition(std::max(0, current)); getWidget(w, "FatigueFrame"); w->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } } void HUD::setDrowningTimeLeft(float time, float maxTime) { size_t progress = static_cast(time / maxTime * 200); mDrowning->setProgressPosition(progress); bool isDrowning = (progress == 0); if (isDrowning && !mIsDrowning) // Just started drowning mDrowningFlashTheta = 0.0f; // Start out on bright red every time. mDrowningFlash->setVisible(isDrowning); mIsDrowning = isDrowning; } void HUD::setDrowningBarVisible(bool visible) { mDrowningBar->setVisible(visible); } void HUD::onWorldClicked(MyGUI::Widget* _sender) { if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) return; MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); if (mDragAndDrop->mIsOnDragAndDrop) { // drop item into the gameworld MWBase::Environment::get().getWorld()->breakInvisibility(MWMechanics::getPlayer()); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint cursorPosition = MyGUI::InputManager::getInstance().getMousePosition(); float mouseX = cursorPosition.left / float(viewSize.width); float mouseY = cursorPosition.top / float(viewSize.height); WorldItemModel drop(mouseX, mouseY); mDragAndDrop->drop(&drop, nullptr); winMgr->changePointer("arrow"); } else { GuiMode mode = winMgr->getMode(); if (!winMgr->isConsoleMode() && (mode != GM_Container) && (mode != GM_Inventory)) return; MWWorld::Ptr object = MWBase::Environment::get().getWorld()->getFacedObject(); if (winMgr->isConsoleMode()) winMgr->setConsoleSelectedObject(object); else // if ((mode == GM_Container) || (mode == GM_Inventory)) { // pick up object if (!object.isEmpty()) winMgr->getInventoryWindow()->pickUpObject(object); } } } void HUD::onWorldMouseOver(MyGUI::Widget* _sender, int x, int y) { if (mDragAndDrop->mIsOnDragAndDrop) { mWorldMouseOver = false; 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); MWBase::World* world = MWBase::Environment::get().getWorld(); // if we can't drop the object at the wanted position, show the "drop on ground" cursor. bool canDrop = world->canPlaceObject(mouseX, mouseY); if (!canDrop) MWBase::Environment::get().getWindowManager()->changePointer("drop_ground"); else MWBase::Environment::get().getWindowManager()->changePointer("arrow"); } else { MWBase::Environment::get().getWindowManager()->changePointer("arrow"); mWorldMouseOver = true; } } void HUD::onWorldMouseLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _new) { MWBase::Environment::get().getWindowManager()->changePointer("arrow"); mWorldMouseOver = false; } void HUD::onHMSClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); } void HUD::onMapClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); } void HUD::onWeaponClicked(MyGUI::Widget* _sender) { const MWWorld::Ptr& player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return; } MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } void HUD::onMagicClicked(MyGUI::Widget* _sender) { const MWWorld::Ptr& player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return; } MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); } void HUD::setCellName(const std::string& cellName) { if (mCellName != cellName) { mCellNameTimer = 5.0f; mCellName = cellName; mCellNameBox->setCaptionWithReplacing("#{sCell=" + mCellName + "}"); mCellNameBox->setVisible(mMapVisible); } } void HUD::onFrame(float dt) { LocalMapBase::onFrame(dt); mCellNameTimer -= dt; mWeaponSpellTimer -= dt; if (mCellNameTimer < 0) mCellNameBox->setVisible(false); if (mWeaponSpellTimer < 0) mWeaponSpellBox->setVisible(false); mEnemyHealthTimer -= dt; if (mEnemyHealth->getVisible() && mEnemyHealthTimer < 0) { mEnemyHealth->setVisible(false); mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() + MyGUI::IntPoint(0, 20)); } mSpellIcons->updateWidgets(mEffectBox, true); if (mEnemyActorId != -1 && mEnemyHealth->getVisible()) { 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 ESM::RefId& spellId, int successChancePercent) { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); const std::string& spellName = spell->mName; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = spellName; mWeaponSpellBox->setCaption(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(successChancePercent); mSpellBox->setUserString("ToolTipType", "Spell"); mSpellBox->setUserString("Spell", spellId.serialize()); mSpellBox->setUserData(MyGUI::Any::Null); 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_view itemName = item.getClass().getName(item); if (itemName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = itemName; mWeaponSpellBox->setCaption(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(chargePercent); mSpellBox->setUserString("ToolTipType", "ItemPtr"); mSpellBox->setUserData(MWWorld::Ptr(item)); mSpellImage->setItem(item); } void HUD::setSelectedWeapon(const MWWorld::Ptr& item, int durabilityPercent) { std::string_view itemName = item.getClass().getName(item); if (itemName != mWeaponName && mWeaponVisible) { mWeaponSpellTimer = 5.0f; mWeaponName = itemName; mWeaponSpellBox->setCaption(mWeaponName); mWeaponSpellBox->setVisible(true); } mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "ItemPtr"); mWeapBox->setUserData(MWWorld::Ptr(item)); mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(durabilityPercent); mWeapImage->setItem(item); } void HUD::unsetSelectedSpell() { std::string_view spellName = "#{Interface:None}"; if (spellName != mSpellName && mSpellVisible) { mWeaponSpellTimer = 5.0f; mSpellName = spellName; mWeaponSpellBox->setCaptionWithReplacing(mSpellName); mWeaponSpellBox->setVisible(true); } mSpellStatus->setProgressRange(100); mSpellStatus->setProgressPosition(0); mSpellImage->setItem(MWWorld::Ptr()); mSpellBox->clearUserStrings(); mSpellBox->setUserData(MyGUI::Any::Null); } void HUD::unsetSelectedWeapon() { std::string itemName = "#{sSkillHandtohand}"; if (itemName != mWeaponName && mWeaponVisible) { mWeaponSpellTimer = 5.0f; mWeaponName = itemName; mWeaponSpellBox->setCaptionWithReplacing(mWeaponName); mWeaponSpellBox->setVisible(true); } mWeapStatus->setProgressRange(100); mWeapStatus->setProgressPosition(0); 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"; mWeapImage->setIcon(icon); mWeapBox->clearUserStrings(); mWeapBox->setUserString("ToolTipType", "Layout"); 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); } void HUD::setCrosshairOwned(bool owned) { if (owned) { mCrosshair->changeWidgetSkin("HUD_Crosshair_Owned"); } else { mCrosshair->changeWidgetSkin("HUD_Crosshair"); } } void HUD::setHmsVisible(bool visible) { mHealth->setVisible(visible); mMagicka->setVisible(visible); mStamina->setVisible(visible); updatePositions(); } void HUD::setWeapVisible(bool visible) { mWeapBox->setVisible(visible); updatePositions(); } void HUD::setSpellVisible(bool visible) { mSpellBox->setVisible(visible); updatePositions(); } void HUD::setSneakVisible(bool visible) { mSneakBox->setVisible(visible); updatePositions(); } void HUD::setEffectVisible(bool visible) { mEffectBox->setVisible(visible); updatePositions(); } void HUD::setMinimapVisible(bool visible) { mMinimapBox->setVisible(visible); updatePositions(); } void HUD::updatePositions() { int weapDx = 0, spellDx = 0, sneakDx = 0; if (!mHealth->getVisible()) sneakDx = spellDx = weapDx = mWeapBoxBaseLeft - mHealthManaStaminaBaseLeft; if (!mWeapBox->getVisible()) { spellDx += mSpellBoxBaseLeft - mWeapBoxBaseLeft; sneakDx = spellDx; } if (!mSpellBox->getVisible()) sneakDx += mSneakBoxBaseLeft - mSpellBoxBaseLeft; mWeaponVisible = mWeapBox->getVisible(); mSpellVisible = mSpellBox->getVisible(); if (!mWeaponVisible && !mSpellVisible) mWeaponSpellBox->setVisible(false); mWeapBox->setPosition(mWeapBoxBaseLeft - weapDx, mWeapBox->getTop()); mSpellBox->setPosition(mSpellBoxBaseLeft - spellDx, mSpellBox->getTop()); mSneakBox->setPosition(mSneakBoxBaseLeft - sneakDx, mSneakBox->getTop()); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); // effect box can have variable width -> variable left coordinate int effectsDx = 0; if (!mMinimapBox->getVisible()) effectsDx = mEffectBoxBaseRight - mMinimapBoxBaseRight; mMapVisible = mMinimapBox->getVisible(); if (!mMapVisible) mCellNameBox->setVisible(false); mEffectBox->setPosition( (viewSize.width - mEffectBoxBaseRight) - mEffectBox->getWidth() + effectsDx, mEffectBox->getTop()); } void HUD::updateEnemyHealthBar() { MWWorld::Ptr enemy = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mEnemyActorId); if (enemy.isEmpty()) return; MWMechanics::CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); 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().getRatio() * 100)); static const float fNPCHealthBarFade = MWBase::Environment::get() .getESMStore() ->get() .find("fNPCHealthBarFade") ->mValue.getFloat(); if (fNPCHealthBarFade > 0.f) mEnemyHealth->setAlpha(std::clamp(mEnemyHealthTimer / fNPCHealthBarFade, 0.f, 1.f)); } void HUD::setEnemy(const MWWorld::Ptr& enemy) { mEnemyActorId = enemy.getClass().getCreatureStats(enemy).getActorId(); mEnemyHealthTimer = MWBase::Environment::get() .getESMStore() ->get() .find("fNPCHealthBarTime") ->mValue.getFloat(); if (!mEnemyHealth->getVisible()) mWeaponSpellBox->setPosition(mWeaponSpellBox->getPosition() - MyGUI::IntPoint(0, 20)); mEnemyHealth->setVisible(true); updateEnemyHealthBar(); } void HUD::clear() { mEnemyActorId = -1; mEnemyHealthTimer = -1; 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) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } void HUD::doorMarkerCreated(MyGUI::Widget* marker) { marker->eventMouseButtonClick += MyGUI::newDelegate(this, &HUD::onMapClicked); } } openmw-openmw-0.49.0/apps/openmw/mwgui/hud.hpp000066400000000000000000000075651503074453300213260ustar00rootroot00000000000000#ifndef OPENMW_GAME_MWGUI_HUD_H #define OPENMW_GAME_MWGUI_HUD_H #include #include "mapwindow.hpp" #include "spellicons.hpp" #include "statswatcher.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class DragAndDrop; class ItemWidget; class SpellWidget; class HUD : public WindowBase, public LocalMapBase, public StatsListener { public: HUD(CustomMarkerCollection& customMarkers, DragAndDrop* dragAndDrop, MWRender::LocalMap* localMapRender); virtual ~HUD(); 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 /// @param maxTime how long we can be underwater (in total) until drowning starts void setDrowningTimeLeft(float time, float maxTime); void setDrowningBarVisible(bool visible); void setHmsVisible(bool visible); void setWeapVisible(bool visible); void setSpellVisible(bool visible); void setSneakVisible(bool visible); void setEffectVisible(bool visible); void setMinimapVisible(bool visible); 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); void unsetSelectedSpell(); void unsetSelectedWeapon(); void setCrosshairVisible(bool visible); void setCrosshairOwned(bool owned); void onFrame(float dt) override; void setCellName(const std::string& cellName); bool getWorldMouseOver() { return mWorldMouseOver; } MyGUI::Widget* getEffectBox() { return mEffectBox; } void setEnemy(const MWWorld::Ptr& enemy); void clear() override; private: MyGUI::ProgressBar *mHealth, *mMagicka, *mStamina, *mEnemyHealth, *mDrowning; MyGUI::Widget* mHealthFrame; MyGUI::Widget *mWeapBox, *mSpellBox, *mSneakBox; ItemWidget* mWeapImage; SpellWidget* mSpellImage; MyGUI::ProgressBar *mWeapStatus, *mSpellStatus; MyGUI::Widget *mEffectBox, *mMinimapBox; MyGUI::Button* mMinimapButton; MyGUI::ScrollView* mMinimap; MyGUI::ImageBox* mCrosshair; MyGUI::TextBox* mCellNameBox; MyGUI::TextBox* mWeaponSpellBox; MyGUI::Widget *mDrowningBar, *mDrowningFrame, *mDrowningFlash; // bottom left elements int mHealthManaStaminaBaseLeft, mWeapBoxBaseLeft, mSpellBoxBaseLeft, mSneakBoxBaseLeft; // bottom right elements int mMinimapBoxBaseRight, mEffectBoxBaseRight; DragAndDrop* mDragAndDrop; std::string mCellName; float mCellNameTimer; std::string mWeaponName; std::string mSpellName; float mWeaponSpellTimer; bool mMapVisible; bool mWeaponVisible; bool mSpellVisible; bool mWorldMouseOver; std::unique_ptr mSpellIcons; int mEnemyActorId; float mEnemyHealthTimer; bool mIsDrowning; float mDrowningFlashTheta; void onWorldClicked(MyGUI::Widget* _sender); void onWorldMouseOver(MyGUI::Widget* _sender, int x, int y); void onWorldMouseLostFocus(MyGUI::Widget* _sender, MyGUI::Widget* _new); void onHMSClicked(MyGUI::Widget* _sender); void onWeaponClicked(MyGUI::Widget* _sender); void onMagicClicked(MyGUI::Widget* _sender); void onMapClicked(MyGUI::Widget* _sender); // LocalMapBase void customMarkerCreated(MyGUI::Widget* marker) override; void doorMarkerCreated(MyGUI::Widget* marker) override; void updateEnemyHealthBar(); void updatePositions(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/inventoryitemmodel.cpp000066400000000000000000000117341503074453300244670ustar00rootroot00000000000000#include "inventoryitemmodel.hpp" #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.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" 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(const ItemStack& item) { size_t i = 0; for (ItemStack& itemStack : mItems) { if (itemStack == item) return i; ++i; } return -1; } MWWorld::Ptr InventoryItemModel::addItem(const ItemStack& item, size_t count, bool allowAutoEquip) { 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::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::ManualRef newRef(*MWBase::Environment::get().getESMStore(), item.mBase, count); return *mActor.getClass().getContainerStore(mActor).add(newRef.getPtr(), count, 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)) { 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); } std::stringstream error; 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) { // 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(); return ItemModel::moveItem(item, count, otherModel, allowAutoEquip); } void InventoryItemModel::update() { MWWorld::ContainerStore& store = mActor.getClass().getContainerStore(mActor); mItems.clear(); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { 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); } } 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); return true; } bool InventoryItemModel::usesContainer(const MWWorld::Ptr& container) { return mActor == container; } } openmw-openmw-0.49.0/apps/openmw/mwgui/inventoryitemmodel.hpp000066400000000000000000000022171503074453300244700ustar00rootroot00000000000000#ifndef MWGUI_INVENTORY_ITEM_MODEL_H #define MWGUI_INVENTORY_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class InventoryItemModel : public ItemModel { public: InventoryItemModel(const MWWorld::Ptr& actor); ItemStack getItem(ModelIndex index) override; ModelIndex getIndex(const ItemStack& item) override; size_t getItemCount() override; bool onTakeItem(const MWWorld::Ptr& item, int 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, bool allowAutoEquip = true) override; void update() override; bool usesContainer(const MWWorld::Ptr& container) override; protected: MWWorld::Ptr mActor; private: std::vector mItems; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/inventorywindow.cpp000066400000000000000000000773571503074453300240340ustar00rootroot00000000000000#include "inventorywindow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/action.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.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 "tradewindow.hpp" namespace { bool isRightHandWeapon(const MWWorld::Ptr& item) { 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); } } 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) : WindowPinnableBase("openmw_inventory_window.layout") , mDragAndDrop(dragAndDrop) , mSelectedItem(-1) , mSortModel(nullptr) , mTradeModel(nullptr) , mGuiMode(GM_Inventory) , mLastXSize(0) , mLastYSize(0) , mPreview(std::make_unique(parent, resourceSystem, MWMechanics::getPlayer())) , mTrading(false) , mUpdateTimer(0.f) { mPreviewTexture = std::make_unique(mPreview->getTexture(), mPreview->getTextureStateSet()); mPreview->rebuild(); mMainWidget->castType()->eventWindowChangeCoord += MyGUI::newDelegate(this, &InventoryWindow::onWindowResize); getWidget(mAvatar, "Avatar"); getWidget(mAvatarImage, "AvatarImage"); getWidget(mEncumbranceBar, "EncumbranceBar"); getWidget(mFilterAll, "AllButton"); getWidget(mFilterWeapon, "WeaponButton"); getWidget(mFilterApparel, "ApparelButton"); getWidget(mFilterMagic, "MagicButton"); getWidget(mFilterMisc, "MiscButton"); getWidget(mLeftPane, "LeftPane"); getWidget(mRightPane, "RightPane"); getWidget(mArmorRating, "ArmorRating"); getWidget(mFilterEdit, "FilterEdit"); mAvatarImage->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onAvatarClicked); mAvatarImage->setRenderItemTexture(mPreviewTexture.get()); mAvatarImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &InventoryWindow::onItemSelected); mItemView->eventBackgroundClicked += MyGUI::newDelegate(this, &InventoryWindow::onBackgroundSelected); mFilterAll->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterWeapon->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &InventoryWindow::onFilterChanged); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &InventoryWindow::onNameFilterChanged); mFilterAll->setStateSelected(true); setGuiMode(mGuiMode); adjustPanes(); } void InventoryWindow::adjustPanes() { 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); } void InventoryWindow::updatePlayer() { 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(std::move(tradeModel)); else { auto sortModel = std::make_unique(std::move(tradeModel)); mSortModel = sortModel.get(); mItemView->setModel(std::move(sortModel)); } mSortModel->setNameFilter(mFilterEdit->getCaption()); mFilterAll->setStateSelected(true); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); mPreview->updatePtr(mPtr); mPreview->rebuild(); mPreview->update(); dirtyPreview(); updatePreviewSize(); updateEncumbranceBar(); mItemView->update(); notifyContentChanged(); } void InventoryWindow::clear() { mPtr = MWWorld::Ptr(); mTradeModel = nullptr; mSortModel = nullptr; mItemView->setModel(nullptr); } void InventoryWindow::toggleMaximized() { const WindowSettingValues settings = getModeSettings(mGuiMode); const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mRegular : settings.mMaximized; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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); settings.mIsMaximized.set(!settings.mIsMaximized); adjustPanes(); updatePreviewSize(); } void InventoryWindow::setGuiMode(GuiMode mode) { mGuiMode = mode; const WindowSettingValues settings = getModeSettings(mGuiMode); setPinButtonVisible(mode == GM_Inventory); const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mMaximized : settings.mRegular; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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()); mMainWidget->setPosition(pos); mMainWidget->setSize(size); adjustPanes(); if (needUpdate) updatePreviewSize(); } SortFilterItemModel* InventoryWindow::getSortFilterModel() { return mSortModel; } TradeItemModel* InventoryWindow::getTradeModel() { return mTradeModel; } ItemModel* InventoryWindow::getModel() { return mTradeModel; } void InventoryWindow::onBackgroundSelected() { if (mDragAndDrop->mIsOnDragAndDrop) mDragAndDrop->drop(mTradeModel, mItemView); } void InventoryWindow::onItemSelected(int index) { onItemSelectedFromSourceModel(mSortModel->mapToSource(index)); } void InventoryWindow::onItemSelectedFromSourceModel(int index) { if (mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->drop(mTradeModel, mItemView); return; } const ItemStack& item = mTradeModel->getItem(index); const ESM::RefId& sound = item.mBase.getClass().getDownSoundId(item.mBase); MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (mTrading) { // Can't give conjured items to a merchant if (item.mFlags & ItemStack::Flag_Bound) { MWBase::Environment::get().getWindowManager()->playSound(sound); MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog9}"); return; } // check if merchant accepts item int services = MWBase::Environment::get().getWindowManager()->getTradeWindow()->getMerchantServices(); if (!object.getClass().canSell(object, services)) { MWBase::Environment::get().getWindowManager()->playSound(sound); MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog4}"); return; } } // If we unequip weapon during attack, it can lead to unexpected behaviour if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPtr)) { bool isWeapon = item.mBase.getType() == ESM::Weapon::sRecordId; MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); if (isWeapon && invStore.isEquipped(item.mBase)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sCantEquipWeapWarning}"); return; } } if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = mTrading ? "#{sQuanityMenuMessage01}" : "#{sTake}"; std::string name{ object.getClass().getName(object) }; name += MWGui::ToolTips::getSoulString(object.getCellRef()); dialog->openCountDialog(name, message, count); dialog->eventOkClicked.clear(); if (mTrading) dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::sellItem); else dialog->eventOkClicked += MyGUI::newDelegate(this, &InventoryWindow::dragItem); mSelectedItem = index; } else { mSelectedItem = index; if (mTrading) sellItem(nullptr, count); else dragItem(nullptr, count); } } void InventoryWindow::ensureSelectedItemUnequipped(int count) { const ItemStack& item = mTradeModel->getItem(mSelectedItem); if (item.mType == ItemStack::Type_Equipped) { MWWorld::InventoryStore& invStore = mPtr.getClass().getInventoryStore(mPtr); 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... // 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) { if (mTradeModel->getItem(i).mBase == newStack) { newIndex = i; break; } } if (newIndex == -1) throw std::runtime_error("Can't find restacked item"); mSelectedItem = newIndex; } } } void InventoryWindow::dragItem(MyGUI::Widget* sender, int count) { ensureSelectedItemUnequipped(count); mDragAndDrop->startDrag(mSelectedItem, mSortModel, mTradeModel, mItemView, count); notifyContentChanged(); } void InventoryWindow::sellItem(MyGUI::Widget* sender, int count) { ensureSelectedItemUnequipped(count); const ItemStack& item = mTradeModel->getItem(mSelectedItem); const ESM::RefId& sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the merchant mTradeModel->returnItemBorrowedToUs(mSelectedItem, count); MWBase::Environment::get().getWindowManager()->getTradeWindow()->returnItem(mSelectedItem, count); } else { // borrow item to the merchant mTradeModel->borrowItemFromUs(mSelectedItem, count); MWBase::Environment::get().getWindowManager()->getTradeWindow()->borrowItem(mSelectedItem, count); } mItemView->update(); notifyContentChanged(); } void InventoryWindow::updateItemView() { MWBase::Environment::get().getWindowManager()->updateSpellWindow(); mItemView->update(); dirtyPreview(); } void InventoryWindow::onOpen() { // Reset the filter focus when opening the window MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (focus == mFilterEdit) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); if (!mPtr.isEmpty()) { updateEncumbranceBar(); mItemView->update(); notifyContentChanged(); } adjustPanes(); } void InventoryWindow::onWindowResize(MyGUI::Window* _sender) { WindowBase::clampWindowCoordinates(_sender); adjustPanes(); const WindowSettingValues settings = getModeSettings(mGuiMode); MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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) { mLastXSize = mMainWidget->getSize().width; mLastYSize = mMainWidget->getSize().height; updatePreviewSize(); updateArmorRating(); } } void InventoryWindow::updateArmorRating() { if (mPtr.isEmpty()) return; 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)))); } void InventoryWindow::updatePreviewSize() { 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) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } void InventoryWindow::onFilterChanged(MyGUI::Widget* _sender) { if (_sender == mFilterAll) mSortModel->setCategory(SortFilterItemModel::Category_All); else if (_sender == mFilterWeapon) mSortModel->setCategory(SortFilterItemModel::Category_Weapon); else if (_sender == mFilterApparel) mSortModel->setCategory(SortFilterItemModel::Category_Apparel); else if (_sender == mFilterMagic) mSortModel->setCategory(SortFilterItemModel::Category_Magic); else if (_sender == mFilterMisc) mSortModel->setCategory(SortFilterItemModel::Category_Misc); mFilterAll->setStateSelected(false); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); mItemView->update(); _sender->castType()->setStateSelected(true); } void InventoryWindow::onPinToggled() { Settings::windows().mInventoryPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setWeaponVisibility(!mPinned); } void InventoryWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) toggleMaximized(); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } void InventoryWindow::useItem(const MWWorld::Ptr& ptr, bool force) { const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) { // Don't try to equip the item if PCSkipEquip is set to 1 if (ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 1) { ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); return; } ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } 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 if (!ptr.getClass().getEquipmentSlots(ptr).first.empty()) { if (ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); updateItemView(); return; } if (!force) { auto canEquip = ptr.getClass().canBeEquipped(ptr, player); if (canEquip.first == 0) { MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second); updateItemView(); return; } } } // If the item has a script, set OnPCEquip or PCSkipEquip to 1 if (!script.empty()) { // Ingredients, books and repair hammers must not have OnPCEquip set to 1 here auto type = ptr.getType(); bool isBook = type == ESM::Book::sRecordId; if (!isBook && type != ESM::Ingredient::sRecordId && type != ESM::Repair::sRecordId) ptr.getRefData().getLocals().setVarByInt(script, "onpcequip", 1); // Books must have PCSkipEquip set to 1 instead else if (isBook) ptr.getRefData().getLocals().setVarByInt(script, "pcskipequip", 1); } 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(); notifyContentChanged(); } // else: will be updated in open() } void InventoryWindow::onAvatarClicked(MyGUI::Widget* _sender) { if (mDragAndDrop->mIsOnDragAndDrop) { MWWorld::Ptr ptr = mDragAndDrop->mItem.mBase; mDragAndDrop->finish(); if (mDragAndDrop->mSourceModel != mTradeModel) { // Move item to the player's inventory 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; } MWBase::Environment::get().getLuaManager()->useItem(ptr, MWMechanics::getPlayer(), false); // If item is ingredient or potion don't stop drag and drop to simplify action of taking more than one 1 // item if ((ptr.getType() == ESM::Potion::sRecordId || ptr.getType() == ESM::Ingredient::sRecordId) && mDragAndDrop->mDraggedCount > 1) { // Item can be provided from other window for example container. // But after DragAndDrop::startDrag item automaticly always gets to player inventory. mSelectedItem = getModel()->getIndex(mDragAndDrop->mItem); dragItem(nullptr, mDragAndDrop->mDraggedCount - 1); } } else { 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()) return; for (size_t i = 0; i < mTradeModel->getItemCount(); ++i) { if (mTradeModel->getItem(i).mBase == itemSelected) { onItemSelectedFromSourceModel(i); return; } } throw std::runtime_error("Can't find clicked item"); } } MWWorld::Ptr InventoryWindow::getAvatarSelectedItem(int x, int 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()) { MWWorld::Ptr item = *invStore.getSlot(slot); if (!item.getClass().showsInInventory(item)) return MWWorld::Ptr(); return item; } return MWWorld::Ptr(); } void InventoryWindow::updateEncumbranceBar() { MWWorld::Ptr player = MWMechanics::getPlayer(); float capacity = player.getClass().getCapacity(player); float encumbrance = player.getClass().getEncumbrance(player); mTradeModel->adjustEncumbrance(encumbrance); mEncumbranceBar->setValue(std::ceil(encumbrance), static_cast(capacity)); } void InventoryWindow::onFrame(float dt) { updateEncumbranceBar(); if (mPinned) { mUpdateTimer += dt; if (0.1f < mUpdateTimer) { mUpdateTimer = 0; // Update pinned inventory in-game if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) { mItemView->update(); notifyContentChanged(); } } } } void InventoryWindow::setTrading(bool trading) { mTrading = trading; } void InventoryWindow::dirtyPreview() { mPreview->update(); updateArmorRating(); } void InventoryWindow::notifyContentChanged() { // 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()); dirtyPreview(); } 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 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.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, count); // remove from world MWBase::Environment::get().getWorld()->deleteObject(object); // get ModelIndex to the item mTradeModel->update(); 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(); } void InventoryWindow::cycle(bool next) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; 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(std::make_unique(player)); model.setSortByType(false); model.update(); if (model.getItemCount() == 0) return; for (ItemModel::ModelIndex i = 0; i < int(model.getItemCount()); ++i) { MWWorld::Ptr item = model.getItem(i).mBase; if (model.getItem(i).mType & ItemStack::Type_Equipped && isRightHandWeapon(item)) selected = i; } int incr = next ? 1 : -1; bool found = false; ESM::RefId lastId; if (selected != -1) lastId = model.getItem(selected).mBase.getCellRef().getRefId(); ItemModel::ModelIndex cycled = selected; for (size_t i = 0; i < model.getItemCount(); ++i) { cycled += incr; cycled = (cycled + model.getItemCount()) % model.getItemCount(); MWWorld::Ptr item = model.getItem(cycled).mBase; // skip different stacks of the same item, or we will get stuck as stacking/unstacking them may change their // relative ordering if (lastId == item.getCellRef().getRefId()) continue; lastId = item.getCellRef().getRefId(); if (item.getClass().getType() == ESM::Weapon::sRecordId && isRightHandWeapon(item) && item.getClass().canBeEquipped(item, player).first) { found = true; break; } } if (!found || selected == cycled) return; useItem(model.getItem(cycled).mBase); } void InventoryWindow::rebuildAvatar() { mPreview->rebuild(); } 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)); } } openmw-openmw-0.49.0/apps/openmw/mwgui/inventorywindow.hpp000066400000000000000000000067771503074453300240370ustar00rootroot00000000000000#ifndef MGUI_Inventory_H #define MGUI_Inventory_H #include "mode.hpp" #include "windowpinnablebase.hpp" #include "../mwrender/characterpreview.hpp" #include "../mwworld/ptr.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { namespace Widgets { class MWDynamicStat; } class ItemView; class SortFilterItemModel; class TradeItemModel; class DragAndDrop; class ItemModel; class InventoryWindow : public WindowPinnableBase { public: InventoryWindow(DragAndDrop* dragAndDrop, osg::Group* parent, Resource::ResourceSystem* resourceSystem); void onOpen() override; /// start trading, disables item drag&drop void setTrading(bool trading); void onFrame(float dt) override; void pickUpObject(MWWorld::Ptr object); MWWorld::Ptr getAvatarSelectedItem(int x, int y); void rebuildAvatar(); SortFilterItemModel* getSortFilterModel(); TradeItemModel* getTradeModel(); ItemModel* getModel(); void updateItemView(); void updatePlayer(); void clear() override; void useItem(const MWWorld::Ptr& ptr, bool force = false); void setGuiMode(GuiMode mode); /// Cycle to previous/next weapon void cycle(bool next); std::string_view getWindowIdForLua() const override { return "Inventory"; } protected: void onTitleDoubleClicked() override; private: DragAndDrop* mDragAndDrop; int mSelectedItem; std::optional mEquippedStackableCount; MWWorld::Ptr mPtr; MWGui::ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; MyGUI::Widget* mAvatar; MyGUI::ImageBox* mAvatarImage; MyGUI::TextBox* mArmorRating; Widgets::MWDynamicStat* mEncumbranceBar; MyGUI::Widget* mLeftPane; MyGUI::Widget* mRightPane; MyGUI::Button* mFilterAll; MyGUI::Button* mFilterWeapon; MyGUI::Button* mFilterApparel; MyGUI::Button* mFilterMagic; MyGUI::Button* mFilterMisc; MyGUI::EditBox* mFilterEdit; GuiMode mGuiMode; int mLastXSize; int mLastYSize; std::unique_ptr mPreviewTexture; std::unique_ptr mPreview; bool mTrading; float mUpdateTimer; void toggleMaximized(); void onItemSelected(int index); void onItemSelectedFromSourceModel(int index); void onBackgroundSelected(); 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 updateEncumbranceBar(); void notifyContentChanged(); void dirtyPreview(); void updatePreviewSize(); void updateArmorRating(); MyGUI::IntSize getPreviewViewportSize() const; osg::Vec2f mapPreviewWindowToViewport(int x, int y) const; 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); }; } #endif // Inventory_H openmw-openmw-0.49.0/apps/openmw/mwgui/itemchargeview.cpp000066400000000000000000000163431503074453300235360ustar00rootroot00000000000000#include "itemchargeview.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwmechanics/spellutil.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "itemmodel.hpp" #include "itemwidget.hpp" namespace MWGui { ItemChargeView::ItemChargeView() : mScrollView(nullptr) , mDisplayMode(DisplayMode_Health) { } void ItemChargeView::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void ItemChargeView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item charge view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void ItemChargeView::setModel(ItemModel* model) { mModel.reset(model); } void ItemChargeView::setDisplayMode(ItemChargeView::DisplayMode type) { mDisplayMode = type; update(); } void ItemChargeView::update() { if (!mModel.get()) { layoutWidgets(); return; } mModel->update(); Lines lines; std::set visitedLines; for (size_t i = 0; i < mModel->getItemCount(); ++i) { ItemStack stack = mModel->getItem(static_cast(i)); bool found = false; for (Lines::const_iterator iter = mLines.begin(); iter != mLines.end(); ++iter) { if (iter->mItemPtr == stack.mBase) { found = true; visitedLines.insert(iter); // update line widgets updateLine(*iter); lines.push_back(*iter); break; } } if (!found) { // add line widgets Line line; line.mItemPtr = stack.mBase; 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->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->setNeedMouseFocus(false); updateLine(line); lines.push_back(line); } } for (Lines::iterator iter = mLines.begin(); iter != mLines.end(); ++iter) { if (visitedLines.count(iter)) continue; // remove line widgets MyGUI::Gui::getInstance().destroyWidget(iter->mText); MyGUI::Gui::getInstance().destroyWidget(iter->mIcon); MyGUI::Gui::getInstance().destroyWidget(iter->mCharge); } 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(); } void ItemChargeView::layoutWidgets() { int currentY = 0; for (Line& line : mLines) { 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); currentY += 32 + 4; } // 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->setVisibleVScroll(true); } void ItemChargeView::resetScrollbars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } void ItemChargeView::setSize(const MyGUI::IntSize& value) { bool changed = (value.width != getWidth() || value.height != getHeight()); Base::setSize(value); if (changed) layoutWidgets(); } void ItemChargeView::setCoord(const MyGUI::IntCoord& value) { bool changed = (value.width != getWidth() || value.height != getHeight()); Base::setCoord(value); if (changed) layoutWidgets(); } void ItemChargeView::updateLine(const ItemChargeView::Line& line) { std::string_view name = line.mItemPtr.getClass().getName(line.mItemPtr); line.mText->setCaption(MyGUI::UString(name)); line.mCharge->setVisible(false); switch (mDisplayMode) { case DisplayMode_Health: if (!line.mItemPtr.getClass().hasItemHealth(line.mItemPtr)) break; line.mCharge->setVisible(true); line.mCharge->setValue(line.mItemPtr.getClass().getItemHealth(line.mItemPtr), line.mItemPtr.getClass().getItemMaxHealth(line.mItemPtr)); break; case DisplayMode_EnchantmentCharge: const ESM::RefId& enchId = line.mItemPtr.getClass().getEnchantment(line.mItemPtr); if (enchId.empty()) break; 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()), MWMechanics::getEnchantmentCharge(*ench)); break; } } void ItemChargeView::onIconClicked(MyGUI::Widget* sender) { eventItemClicked(this, *sender->getUserData()); } void ItemChargeView::onMouseWheelMoved(MyGUI::Widget* /*sender*/, int rel) { 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))); } } openmw-openmw-0.49.0/apps/openmw/mwgui/itemchargeview.hpp000066400000000000000000000032711503074453300235370ustar00rootroot00000000000000#ifndef MWGUI_ITEMCHARGEVIEW_H #define MWGUI_ITEMCHARGEVIEW_H #include #include #include #include "../mwworld/ptr.hpp" #include "widgets.hpp" namespace MyGUI { class TextBox; class ScrollView; } namespace MWGui { class ItemModel; class ItemWidget; class ItemChargeView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemChargeView) public: enum DisplayMode { DisplayMode_Health, DisplayMode_EnchantmentCharge }; ItemChargeView(); /// Register needed components with MyGUI's factory manager static void registerComponents(); void initialiseOverride() override; /// Takes ownership of \a model void setModel(ItemModel* model); void setDisplayMode(DisplayMode type); void update(); void layoutWidgets(); void resetScrollbars(); void setSize(const MyGUI::IntSize& value) override; void setCoord(const MyGUI::IntCoord& value) override; MyGUI::delegates::MultiDelegate eventItemClicked; private: struct Line { MWWorld::Ptr mItemPtr; MyGUI::TextBox* mText; ItemWidget* mIcon; Widgets::MWDynamicStatPtr mCharge; }; void updateLine(const Line& line); void onIconClicked(MyGUI::Widget* sender); void onMouseWheelMoved(MyGUI::Widget* sender, int rel); typedef std::vector Lines; Lines mLines; std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; DisplayMode mDisplayMode; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/itemmodel.cpp000066400000000000000000000114131503074453300225030ustar00rootroot00000000000000#include "itemmodel.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" namespace MWGui { 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).empty()) mFlags |= Flag_Enchanted; if (MWBase::Environment::get().getMechanicsManager()->isBoundItem(base)) mFlags |= Flag_Bound; } ItemStack::ItemStack() : mType(Type_Normal) , mFlags(0) , mCreator(nullptr) , mCount(0) { } bool operator==(const ItemStack& left, const ItemStack& right) { if (left.mType != right.mType) return false; 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); if (left.mBase.getContainerStore()) return left.mBase.getContainerStore()->stacks(left.mBase, right.mBase); if (right.mBase.getContainerStore()) return right.mBase.getContainerStore()->stacks(left.mBase, right.mBase); MWWorld::ContainerStore store; return store.stacks(left.mBase, right.mBase); } ItemModel::ItemModel() {} MWWorld::Ptr ItemModel::moveItem(const ItemStack& item, size_t count, ItemModel* otherModel, bool allowAutoEquip) { 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; } bool ItemModel::allowedToUseItems() const { return true; } bool ItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return true; } bool ItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { return true; } bool ProxyItemModel::allowedToUseItems() const { return mSourceModel->allowedToUseItems(); } MWWorld::Ptr ProxyItemModel::copyItem(const ItemStack& item, size_t count, bool allowAutoEquip) { return mSourceModel->copyItem(item, count, allowAutoEquip); } void ProxyItemModel::removeItem(const ItemStack& item, size_t count) { mSourceModel->removeItem(item, count); } ItemModel::ModelIndex ProxyItemModel::mapToSource(ModelIndex index) { const ItemStack& itemToSearch = getItem(index); for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); if (item.mBase == itemToSearch.mBase) return i; } return -1; } ItemModel::ModelIndex ProxyItemModel::mapFromSource(ModelIndex index) { const ItemStack& itemToSearch = mSourceModel->getItem(index); for (size_t i = 0; i < getItemCount(); ++i) { const ItemStack& item = getItem(i); if (item.mBase == itemToSearch.mBase) return i; } return -1; } ItemModel::ModelIndex ProxyItemModel::getIndex(const ItemStack& item) { return mSourceModel->getIndex(item); } void ProxyItemModel::setSourceModel(std::unique_ptr sourceModel) { mSourceModel = std::move(sourceModel); } void ProxyItemModel::onClose() { mSourceModel->onClose(); } bool ProxyItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onDropItem(item, 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); } } openmw-openmw-0.49.0/apps/openmw/mwgui/itemmodel.hpp000066400000000000000000000076461503074453300225250ustar00rootroot00000000000000#ifndef MWGUI_ITEM_MODEL_H #define MWGUI_ITEM_MODEL_H #include #include "../mwworld/ptr.hpp" namespace MWGui { class ItemModel; /// @brief A single item stack managed by an item model struct ItemStack { ItemStack(const MWWorld::Ptr& base, ItemModel* creator, size_t count); ItemStack(); ///< like operator==, only without checking mType enum Type { Type_Barter, Type_Equipped, Type_Normal }; Type mType; enum Flags { Flag_Enchanted = (1 << 0), Flag_Bound = (1 << 1) }; int mFlags; ItemModel* mCreator; size_t mCount; MWWorld::Ptr mBase; }; bool operator==(const ItemStack& left, const ItemStack& right); /// @brief The base class that all item models should derive from. class ItemModel { public: ItemModel(); virtual ~ItemModel() {} typedef int ModelIndex; // -1 means invalid index /// Throws for invalid index or out of range index 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(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, bool allowAutoEquip = true); 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 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. class ProxyItemModel : public ItemModel { public: 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; 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(std::unique_ptr sourceModel); ModelIndex mapToSource(ModelIndex index); ModelIndex mapFromSource(ModelIndex index); bool usesContainer(const MWWorld::Ptr& container) override; protected: std::unique_ptr mSourceModel; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/itemselection.cpp000066400000000000000000000034251503074453300233740ustar00rootroot00000000000000#include "itemselection.hpp" #include #include #include "inventoryitemmodel.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { ItemSelectionDialog::ItemSelectionDialog(const std::string& label) : WindowModal("openmw_itemselection_dialog.layout") , mSortModel(nullptr) { getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &ItemSelectionDialog::onSelectedItem); MyGUI::TextBox* l; getWidget(l, "Label"); l->setCaptionWithReplacing(label); MyGUI::Button* cancelButton; getWidget(cancelButton, "CancelButton"); cancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ItemSelectionDialog::onCancelButtonClicked); center(); } bool ItemSelectionDialog::exit() { eventDialogCanceled(); return true; } void ItemSelectionDialog::openContainer(const MWWorld::Ptr& container) { auto sortModel = std::make_unique(std::make_unique(container)); mSortModel = sortModel.get(); mItemView->setModel(std::move(sortModel)); mItemView->resetScrollBars(); } void ItemSelectionDialog::setCategory(int category) { mSortModel->setCategory(category); mItemView->update(); } void ItemSelectionDialog::setFilter(int filter) { mSortModel->setFilter(filter); mItemView->update(); } void ItemSelectionDialog::onSelectedItem(int index) { ItemStack item = mSortModel->getItem(index); eventItemSelected(item.mBase); } void ItemSelectionDialog::onCancelButtonClicked(MyGUI::Widget* sender) { exit(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/itemselection.hpp000066400000000000000000000020331503074453300233730ustar00rootroot00000000000000#ifndef OPENMW_GAME_MWGUI_ITEMSELECTION_H #define OPENMW_GAME_MWGUI_ITEMSELECTION_H #include #include "windowbase.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class ItemView; class SortFilterItemModel; class ItemSelectionDialog : public WindowModal { public: ItemSelectionDialog(const std::string& label); bool exit() override; 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 setCategory(int category); void setFilter(int filter); SortFilterItemModel* getSortModel() { return mSortModel; } private: ItemView* mItemView; SortFilterItemModel* mSortModel; void onSelectedItem(int index); void onCancelButtonClicked(MyGUI::Widget* sender); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/itemview.cpp000066400000000000000000000122511503074453300223560ustar00rootroot00000000000000#include "itemview.hpp" #include #include #include #include #include #include "itemmodel.hpp" #include "itemwidget.hpp" namespace MWGui { ItemView::ItemView() : mScrollView(nullptr) { } void ItemView::setModel(std::unique_ptr model) { mModel = std::move(model); update(); } void ItemView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void ItemView::layoutWidgets() { if (!mScrollView->getChildCount()) return; 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; for (unsigned int i = 0; i < dragArea->getChildCount(); ++i) { MyGUI::Widget* w = dragArea->getChildAt(i); w->setPosition(x, y); y += 42; 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); } void ItemView::update() { while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); if (!mModel) return; mModel->update(); 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); 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); } 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::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::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::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/itemview.hpp000066400000000000000000000025531503074453300223670ustar00rootroot00000000000000#ifndef MWGUI_ITEMVIEW_H #define MWGUI_ITEMVIEW_H #include #include "itemmodel.hpp" namespace MWGui { class ItemView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemView) public: ItemView(); /// Register needed components with MyGUI's factory manager static void registerComponents(); /// Takes ownership of \a model void setModel(std::unique_ptr model); 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) EventHandle_Void eventBackgroundClicked; void update(); void resetScrollBars(); private: void initialiseOverride() override; void layoutWidgets(); 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 onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); std::unique_ptr mModel; MyGUI::ScrollView* mScrollView; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/itemwidget.cpp000066400000000000000000000150321503074453300226670ustar00rootroot00000000000000#include "itemwidget.hpp" #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" namespace { std::string getCountString(int count) { if (count == 1) 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"; else if (count > 999999) return MyGUI::utility::toString(count / 1000000) + "m"; else if (count > 9999) return MyGUI::utility::toString(count / 1000) + "k"; else return MyGUI::utility::toString(count); } } namespace MWGui { std::map ItemWidget::mScales; ItemWidget::ItemWidget() : mItem(nullptr) , mItemShadow(nullptr) , mFrame(nullptr) , mText(nullptr) { } void ItemWidget::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void ItemWidget::initialiseOverride() { assignWidget(mItem, "Item"); if (mItem) mItem->setNeedMouseFocus(false); assignWidget(mItemShadow, "ItemShadow"); if (mItemShadow) mItemShadow->setNeedMouseFocus(false); assignWidget(mFrame, "Frame"); if (mFrame) mFrame->setNeedMouseFocus(false); assignWidget(mText, "Text"); if (mText) mText->setNeedMouseFocus(false); Base::initialiseOverride(); } void ItemWidget::setCount(int count) { if (!mText) return; mText->setCaption(getCountString(count)); } void ItemWidget::setIcon(const std::string& icon) { if (mCurrentIcon != icon) { mCurrentIcon = icon; if (mItemShadow) mItemShadow->setImageTexture(icon); if (mItem) mItem->setImageTexture(icon); } } void ItemWidget::setFrame(const std::string& frame, const MyGUI::IntCoord& coord) { if (mFrame) { mFrame->setImageTile(MyGUI::IntSize(coord.width, coord.height)); // Why is this needed? MyGUI bug? mFrame->setImageCoord(coord); } if (mCurrentFrame != frame) { mCurrentFrame = frame; mFrame->setImageTexture(frame); } } void ItemWidget::setIcon(const MWWorld::Ptr& ptr) { 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 = Misc::ResourceHelpers::correctIconPath("default icon.tga", vfs); } setIcon(invIcon); } void ItemWidget::setItem(const MWWorld::Ptr& ptr, ItemState state) { if (!mItem) return; if (ptr.isEmpty()) { if (mFrame) mFrame->setImageTexture({}); if (mItemShadow) mItemShadow->setImageTexture({}); mItem->setImageTexture({}); mText->setCaption({}); mCurrentIcon.clear(); mCurrentFrame.clear(); return; } bool isMagic = !ptr.getClass().getEnchantment(ptr).empty(); std::string backgroundTex = "textures\\menu_icon"; if (isMagic) backgroundTex += "_magic"; if (state == None) { if (!isMagic) backgroundTex.clear(); } else if (state == Equip) { backgroundTex += "_equip"; } else if (state == Barter) backgroundTex += "_barter"; if (!backgroundTex.empty()) backgroundTex += ".dds"; float scale = 1.f; if (!backgroundTex.empty()) { auto found = mScales.find(backgroundTex); if (found == mScales.end()) { // By default, background icons are supposed to use the 42x42 part of 64x64 image. // If the image has a different size, we should use a different chunk size // Cache result to do not retrieve background texture every frame. MyGUI::ITexture* texture = MyGUI::RenderManager::getInstance().getTexture(backgroundTex); if (texture) scale = texture->getHeight() / 64.f; mScales[backgroundTex] = scale; } else scale = found->second; } if (state == Barter && !isMagic) setFrame(backgroundTex, MyGUI::IntCoord(2 * scale, 2 * scale, 44 * scale, 44 * scale)); else setFrame(backgroundTex, MyGUI::IntCoord(0, 0, 44 * scale, 44 * scale)); setIcon(ptr); } void SpellWidget::setSpellIcon(std::string_view icon) { if (mFrame && !mCurrentFrame.empty()) { mCurrentFrame.clear(); mFrame->setImageTexture({}); } if (mCurrentIcon != icon) { mCurrentIcon = icon; if (mItemShadow) mItemShadow->setImageTexture(icon); if (mItem) mItem->setImageTexture(icon); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/itemwidget.hpp000066400000000000000000000027121503074453300226750ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_ITEMWIDGET_H #define OPENMW_MWGUI_ITEMWIDGET_H #include namespace MWWorld { class Ptr; } namespace MWGui { /// @brief A widget that shows an icon for an MWWorld::Ptr class ItemWidget : public MyGUI::Widget { MYGUI_RTTI_DERIVED(ItemWidget) public: ItemWidget(); /// Register needed components with MyGUI's factory manager static void registerComponents(); enum ItemState { None, Equip, Barter, Magic }; /// Set count to be displayed in a textbox over the item void setCount(int count); /// \a ptr may be empty 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); protected: void initialiseOverride() override; MyGUI::ImageBox* mItem; MyGUI::ImageBox* mItemShadow; MyGUI::ImageBox* mFrame; MyGUI::TextBox* mText; std::string mCurrentIcon; std::string mCurrentFrame; static std::map mScales; }; class SpellWidget : public ItemWidget { MYGUI_RTTI_DERIVED(SpellWidget) public: void setSpellIcon(std::string_view icon); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/jailscreen.cpp000066400000000000000000000110441503074453300226430ustar00rootroot00000000000000#include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/store.hpp" #include "jailscreen.hpp" namespace MWGui { JailScreen::JailScreen() : WindowBase("openmw_jail_screen.layout") , mDays(1) , mFadeTimeRemaining(0) , mTimeAdvancer(0.01f) { getWidget(mProgressBar, "ProgressBar"); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &JailScreen::onJailProgressChanged); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &JailScreen::onJailFinished); center(); } void JailScreen::goToJail(int days) { mDays = days; MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.5); mFadeTimeRemaining = 0.5; setVisible(false); mProgressBar->setScrollRange(100 + 1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); } void JailScreen::onFrame(float dt) { mTimeAdvancer.onFrame(dt); if (mFadeTimeRemaining <= 0) return; mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) { MWWorld::Ptr player = MWMechanics::getPlayer(); 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); } } void JailScreen::onJailProgressChanged(int cur, int /*total*/) { mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize( static_cast(cur / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); } void JailScreen::onJailFinished() { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Jail); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.5); MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getMechanicsManager()->rest(mDays * 24, true); MWBase::Environment::get().getWorld()->advanceTime(mDays * 24); // We should not worsen corprus when in prison player.getClass().getCreatureStats(player).getActiveSpells().skipWorsenings(mDays * 24); const auto& skillStore = MWBase::Environment::get().getESMStore()->get(); std::set skills; for (int day = 0; day < mDays; ++day) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); const ESM::Skill* skill = skillStore.searchRandom({}, prng); skills.insert(skill); MWMechanics::SkillValue& value = player.getClass().getNpcStats(player).getSkill(skill->mId); if (skill->mId == ESM::Skill::Security || skill->mId == ESM::Skill::Sneak) value.setBase(std::min(100.f, value.getBase() + 1)); else value.setBase(std::max(0.f, value.getBase() - 1)); } const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); std::string message; if (mDays == 1) message = gmst.find("sNotifyMessage42")->mValue.getString(); else message = gmst.find("sNotifyMessage43")->mValue.getString(); message = Misc::StringUtils::format(message, mDays); for (const ESM::Skill* skill : skills) { int skillValue = player.getClass().getNpcStats(player).getSkill(skill->mId).getBase(); std::string skillMsg = gmst.find("sNotifyMessage44")->mValue.getString(); if (skill->mId == ESM::Skill::Sneak || skill->mId == ESM::Skill::Security) skillMsg = gmst.find("sNotifyMessage39")->mValue.getString(); skillMsg = Misc::StringUtils::format(skillMsg, skill->mName, skillValue); message += "\n" + skillMsg; } std::vector buttons; buttons.emplace_back("#{Interface:OK}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } } openmw-openmw-0.49.0/apps/openmw/mwgui/jailscreen.hpp000066400000000000000000000012351503074453300226510ustar00rootroot00000000000000#ifndef MWGUI_JAILSCREEN_H #define MWGUI_JAILSCREEN_H #include "timeadvancer.hpp" #include "windowbase.hpp" namespace MWGui { class JailScreen : public WindowBase { public: JailScreen(); void goToJail(int days); void onFrame(float dt) override; bool exit() override { return false; } std::string_view getWindowIdForLua() const override { return "JailScreen"; } private: int mDays; float mFadeTimeRemaining; MyGUI::ScrollBar* mProgressBar; void onJailProgressChanged(int cur, int total); void onJailFinished(); TimeAdvancer mTimeAdvancer; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/journalbooks.cpp000066400000000000000000000243261503074453300232430ustar00rootroot00000000000000#include "journalbooks.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include #include "textcolours.hpp" namespace { struct AddContent { MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; 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(std::move(typesetter), body_style) { } 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); mTypesetter->write(style, begin, end); } }; struct AddEntry { MWGui::BookTypesetter::Ptr mTypesetter; MWGui::BookTypesetter::Style* mBodyStyle; AddEntry(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* body_style) : mTypesetter(std::move(typesetter)) , mBodyStyle(body_style) { } void operator()(MWGui::JournalViewModel::Entry const& entry) { mTypesetter->addContent(entry.body()); entry.visitSpans(AddSpan(mTypesetter, mBodyStyle)); } }; struct AddJournalEntry : AddEntry { 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(std::move(typesetter), body_style) , mAddHeader(add_header) , mHeaderStyle(header_style) { } void operator()(MWGui::JournalViewModel::JournalEntry const& entry) { if (mAddHeader) { mTypesetter->write(mHeaderStyle, entry.timestamp()); mTypesetter->lineBreak(); } AddEntry::operator()(entry); mTypesetter->sectionBreak(30); } }; struct AddTopicEntry : AddEntry { 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(std::move(typesetter), body_style) , mContentId(contentId) , mHeaderStyle(header_style) { } void operator()(MWGui::JournalViewModel::TopicEntry const& entry) { mTypesetter->write(mBodyStyle, entry.source()); mTypesetter->write(mBodyStyle, 0, 3); // begin AddEntry::operator()(entry); mTypesetter->selectContent(mContentId); mTypesetter->write(mBodyStyle, 2, 3); // end quote mTypesetter->sectionBreak(30); } }; struct AddTopicName : AddContent { AddTopicName(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : AddContent(std::move(typesetter), style) { } void operator()(MWGui::JournalViewModel::Utf8Span topicName) { mTypesetter->write(mBodyStyle, topicName); mTypesetter->sectionBreak(); } }; struct AddQuestName : AddContent { AddQuestName(MWGui::BookTypesetter::Ptr typesetter, MWGui::BookTypesetter::Style* style) : AddContent(std::move(typesetter), style) { } void operator()(MWGui::JournalViewModel::Utf8Span topicName) { mTypesetter->write(mBodyStyle, topicName); mTypesetter->sectionBreak(); } }; } namespace MWGui { MWGui::BookTypesetter::Utf8Span to_utf8_span(std::string_view text) { typedef MWGui::BookTypesetter::Utf8Point point; point begin = reinterpret_cast(text.data()); return MWGui::BookTypesetter::Utf8Span(begin, begin + text.length()); } typedef TypesetBook::Ptr book; JournalBooks::JournalBooks(JournalViewModel::Ptr model, ToUTF8::FromType encoding) : mModel(std::move(model)) , mEncoding(encoding) , mIndexPagesCount(0) { } 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); 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(); } 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); mModel->visitJournalEntries({}, AddJournalEntry(typesetter, body, header, true)); return typesetter->complete(); } 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); mModel->visitTopicName(topicId, AddTopicName(typesetter, header)); intptr_t contentId = typesetter->addContent(to_utf8_span(", \"")); mModel->visitTopicEntries(topicId, AddTopicEntry(typesetter, body, header, contentId)); return typesetter->complete(); } 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); AddQuestName addName(typesetter, header); addName(to_utf8_span(questName)); mModel->visitJournalEntries(questName, AddJournalEntry(typesetter, body, header, false)); return typesetter->complete(); } book JournalBooks::createTopicIndexBook() { bool isRussian = (mEncoding == ToUTF8::WINDOWS_1251); BookTypesetter::Ptr typesetter = isRussian ? createCyrillicJournalIndex() : createLatinJournalIndex(); return typesetter->complete(); } BookTypesetter::Ptr JournalBooks::createLatinJournalIndex() { BookTypesetter::Ptr typesetter = BookTypesetter::create(92, 260); typesetter->setSectionAlignment(BookTypesetter::AlignCenter); // Latin journal index always has two columns for now. mIndexPagesCount = 2; char ch = 'A'; std::string buffer; 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); if (i == 13) typesetter->sectionBreak(); typesetter->write(style, to_utf8_span(buffer)); typesetter->lineBreak(); ch++; } return typesetter; } BookTypesetter::Ptr JournalBooks::createCyrillicJournalIndex() { BookTypesetter::Ptr typesetter = BookTypesetter::create(92, 260); typesetter->setSectionAlignment(BookTypesetter::AlignCenter); 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; } unsigned char ch[3] = { 0xd0, 0x90, 0x00 }; // CYRILLIC CAPITAL A is a 0xd090 in UTF-8 std::string buffer; for (int i = 0; i < 32; ++i) { buffer = "( "; buffer += ch[0]; buffer += ch[1]; buffer += " )"; Utf8Stream stream(ch, ch + 2); 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); ch[1]++; // Words can not be started with these characters if (i == 26 || i == 28) continue; if (i % sectionBreak == 0) typesetter->sectionBreak(); typesetter->write(style, to_utf8_span(buffer)); typesetter->lineBreak(); } return typesetter; } BookTypesetter::Ptr JournalBooks::createTypesetter() { // TODO: determine page size from layout... return BookTypesetter::create(240, 320); } } openmw-openmw-0.49.0/apps/openmw/mwgui/journalbooks.hpp000066400000000000000000000016601503074453300232440ustar00rootroot00000000000000#ifndef MWGUI_JOURNALBOOKS_HPP #define MWGUI_JOURNALBOOKS_HPP #include "bookpage.hpp" #include "journalviewmodel.hpp" #include namespace MWGui { 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); 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(); }; } #endif // MWGUI_JOURNALBOOKS_HPP openmw-openmw-0.49.0/apps/openmw/mwgui/journalviewmodel.cpp000066400000000000000000000323611503074453300241170ustar00rootroot00000000000000#include "journalviewmodel.hpp" #include #include #include #include #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 { typedef MWDialogue::KeywordSearch KeywordSearchT; mutable bool mKeywordSearchLoaded; mutable KeywordSearchT mKeywordSearch; JournalViewModelImpl() { mKeywordSearchLoaded = false; } virtual ~JournalViewModelImpl() {} /// \todo replace this nasty BS static Utf8Span toUtf8Span(std::string_view str) { if (str.size() == 0) return Utf8Span(Utf8Point(nullptr), Utf8Point(nullptr)); Utf8Point point = reinterpret_cast(str.data()); return Utf8Span(point, point + str.size()); } void load() override {} void unload() override { mKeywordSearch.clear(); mKeywordSearchLoaded = false; } void ensureKeyWordSearchLoaded() const { if (!mKeywordSearchLoaded) { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); for (MWBase::Journal::TTopicIter i = journal->topicBegin(); i != journal->topicEnd(); ++i) mKeywordSearch.seed(i->second.getName(), intptr_t(&i->second)); mKeywordSearchLoaded = true; } } bool isEmpty() const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); return journal->begin() == journal->end(); } template struct BaseEntry : Interface { typedef t_iterator iterator_t; iterator_t itr; JournalViewModelImpl const* mModel; BaseEntry(JournalViewModelImpl const* model, iterator_t itr) : itr(itr) , mModel(model) , loaded(false) { } virtual ~BaseEntry() {} mutable bool loaded; mutable std::string utf8text; typedef std::pair Range; // hyperlinks in @link# notation mutable std::map mHyperLinks; virtual std::string getText() const = 0; void ensureLoaded() const { if (!loaded) { mModel->ensureKeyWordSearchLoaded(); utf8text = getText(); 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; } loaded = true; } } Utf8Span body() const override { ensureLoaded(); return toUtf8Span(utf8text); } void visitSpans(std::function visitor) const override { ensureLoaded(); mModel->ensureKeyWordSearchLoaded(); if (mHyperLinks.size() && MWBase::Environment::get().getWindowManager()->getTranslationDataStorage().hasTranslation()) { 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()); } 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) { const KeywordSearchT::Match& match = *it; if (i != match.mBeg) visitor(0, i - utf8text.begin(), match.mBeg - utf8text.begin()); visitor(match.mValue, match.mBeg - utf8text.begin(), match.mEnd - utf8text.begin()); i = match.mEnd; } if (i != utf8text.end()) visitor(0, i - utf8text.begin(), utf8text.size()); } } }; void visitQuestNames(bool active_only, std::function visitor) const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); 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) { const MWDialogue::Quest& quest = i->second; 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; } if (active_only && isFinished) continue; // 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.emplace(quest.getName()); } } } template struct JournalEntryImpl : BaseEntry { using BaseEntry::itr; mutable std::string timestamp_buffer; JournalEntryImpl(JournalViewModelImpl const* model, iterator_t itr) : BaseEntry(model, itr) { } std::string getText() const override { return itr->getText(); } Utf8Span timestamp() const override { if (timestamp_buffer.empty()) { std::string dayStr = MyGUI::LanguageManager::getInstance().replaceTags("#{sDay}"); std::ostringstream os; os << itr->mDayOfMonth << ' ' << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName(itr->mMonth) << " (" << dayStr << " " << (itr->mDay) << ')'; timestamp_buffer = os.str(); } return toUtf8Span(timestamp_buffer); } }; void visitJournalEntries( std::string_view questName, std::function visitor) const override { MWBase::Journal* journal = MWBase::Environment::get().getJournal(); if (!questName.empty()) { 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); } for (MWBase::Journal::TEntryIter i = journal->begin(); i != journal->end(); ++i) { for (std::vector::iterator questIt = quests.begin(); questIt != quests.end(); ++questIt) { 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)); } } 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 visitor) const override { 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 != Utf8Stream::toLowerUtf8(character)) continue; visitor(i->second.getName()); } } struct TopicEntryImpl : BaseEntry { MWDialogue::Topic const& mTopic; TopicEntryImpl(JournalViewModelImpl const* model, MWDialogue::Topic const& topic, iterator_t itr) : BaseEntry(model, itr) , mTopic(topic) { } std::string getText() const override { return itr->getText(); } Utf8Span source() const override { return toUtf8Span(itr->mActorName); } }; void visitTopicEntries(TopicId topicId, std::function visitor) const override { 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)); } }; JournalViewModel::Ptr JournalViewModel::create() { return std::make_shared(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/journalviewmodel.hpp000066400000000000000000000073421503074453300241250ustar00rootroot00000000000000#ifndef MWGUI_JOURNALVIEWMODEL_HPP #define MWGUI_JOURNALVIEWMODEL_HPP #include #include #include #include #include namespace MWGui { /// View-Model for the journal GUI /// /// This interface defines an abstract data model suited /// specifically to the needs of the journal GUI. It isolates /// the journal GUI from the implementation details of the /// game data store. struct JournalViewModel { typedef std::shared_ptr Ptr; typedef intptr_t QuestId; typedef intptr_t TopicId; typedef uint8_t const* Utf8Point; typedef std::pair Utf8Span; /// The base interface for both journal entries and topics. struct Entry { /// returns the body text for the journal entry /// /// 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; /// 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 ~Entry() = default; }; /// An interface to topic data. struct TopicEntry : Entry { /// 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 ~TopicEntry() = default; }; /// An interface to journal data. struct JournalEntry : Entry { /// 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 ~JournalEntry() = default; }; /// called prior to journal opening virtual void load() = 0; /// called prior to journal closing virtual void unload() = 0; /// returns true if their are no journal entries to display 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; /// 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( 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; /// walks over the topics whose names start with the character 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; // create an instance of the default journal view model implementation static Ptr create(); virtual ~JournalViewModel() = default; }; } #endif // MWGUI_JOURNALVIEWMODEL_HPP openmw-openmw-0.49.0/apps/openmw/mwgui/journalwindow.cpp000066400000000000000000000563771503074453300234500ustar00rootroot00000000000000#include "journalwindow.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/windowmanager.hpp" #include "bookpage.hpp" #include "journalbooks.hpp" #include "journalviewmodel.hpp" #include "windowbase.hpp" namespace { 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 { struct DisplayState { unsigned int mPage; Book mBook; }; typedef std::stack DisplayStateStack; DisplayStateStack mStates; Book mTopicIndexBook; bool mQuestMode; bool mOptionsMode; bool mTopicsMode; bool mAllQuests; template T* getWidget(std::string_view name) { T* widget; WindowBase::getWidget(widget, name); return widget; } template void setText(std::string_view name, value_type const& value) { getWidget(name)->setCaption(MyGUI::utility::toString(value)); } void setVisible(std::string_view name, bool visible) { getWidget(name)->setVisible(visible); } void adviseButtonClick(std::string_view name, void (JournalWindowImpl::*handler)(MyGUI::Widget*)) { getWidget(name)->eventMouseButtonClick += newDelegate(this, handler); } void adviseKeyPress( std::string_view name, void (JournalWindowImpl::*handler)(MyGUI::Widget*, MyGUI::KeyCode, MyGUI::Char)) { getWidget(name)->eventKeyButtonPressed += newDelegate(this, handler); } MWGui::BookPage* getPage(std::string_view name) { return getWidget(name); } 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(TopicsBTN, &JournalWindowImpl::notifyTopics); adviseButtonClick(QuestsBTN, &JournalWindowImpl::notifyQuests); adviseButtonClick(CancelBTN, &JournalWindowImpl::notifyCancel); 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); Gui::MWList* list = getWidget(QuestsList); list->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyQuestClicked); Gui::MWList* topicsList = getWidget(TopicsList); topicsList->eventItemSelected += MyGUI::newDelegate(this, &JournalWindowImpl::notifyTopicSelected); { MWGui::BookPage::ClickCallback callback = [this](intptr_t linkId) { notifyTopicClicked(linkId); }; 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); } { MWGui::BookPage::ClickCallback callback = [this](MWGui::TypesetBook::InteractiveId index) { notifyIndexLinkClicked(index); }; getPage(LeftTopicIndex)->adviseLinkClicked(callback); getPage(CenterTopicIndex)->adviseLinkClicked(callback); getPage(RightTopicIndex)->adviseLinkClicked(std::move(callback)); } adjustButton(PrevPageBTN); float nextButtonScale = adjustButton(NextPageBTN); adjustButton(CloseBTN); adjustButton(CancelBTN); adjustButton(JournalBTN); Gui::ImageButton* optionsButton = getWidget(OptionsBTN); Gui::ImageButton* showActiveButton = getWidget(ShowActiveBTN); Gui::ImageButton* showAllButton = getWidget(ShowAllBTN); Gui::ImageButton* questsButton = getWidget(QuestsBTN); Gui::ImageButton* nextButton = getWidget(NextPageBTN); 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)); } if (!questList) { // If tribunal is not installed (-> no options button), we still want the Topics button available, // so place it where the options button would have been Gui::ImageButton* topicsButton = getWidget(TopicsBTN); topicsButton->detachFromWidget(); topicsButton->attachToWidget(optionsButton->getParent()); topicsButton->setPosition(optionsButton->getPosition()); topicsButton->eventMouseButtonClick.clear(); topicsButton->eventMouseButtonClick += MyGUI::newDelegate(this, &JournalWindowImpl::notifyOptions); optionsButton->setVisible(false); showActiveButton->setVisible(false); showAllButton->setVisible(false); questsButton->setVisible(false); adjustButton(TopicsBTN); } else { optionsButton->setImage("textures/tx_menubook_options.dds"); showActiveButton->setImage("textures/tx_menubook_quests_active.dds"); showAllButton->setImage("textures/tx_menubook_quests_all.dds"); questsButton->setImage("textures/tx_menubook_quests.dds"); adjustButton(ShowAllBTN); adjustButton(ShowActiveBTN); adjustButton(OptionsBTN); adjustButton(QuestsBTN); adjustButton(TopicsBTN); int topicsWidth = getWidget(TopicsBTN)->getSize().width; int cancelLeft = getWidget(CancelBTN)->getPosition().left; int cancelRight = getWidget(CancelBTN)->getPosition().left + getWidget(CancelBTN)->getSize().width; 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. if (topicsWidth == 64) { 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); } } mQuestMode = false; mAllQuests = false; mOptionsMode = false; mTopicsMode = false; } void onOpen() override { mModel->load(); setBookMode(); Book journalBook; if (mModel->isEmpty()) journalBook = createEmptyJournalBook(); else journalBook = createJournalBook(); pushBook(journalBook, 0); // fast forward to the last page if (!mStates.empty()) { unsigned int& page = mStates.top().mPage; page = mStates.top().mBook->pageCount() - 1; if (page % 2) --page; } updateShowingPages(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(getWidget(CloseBTN)); } void onClose() override { mModel->unload(); getPage(LeftBookPage)->showPage(Book(), 0); getPage(RightBookPage)->showPage(Book(), 0); while (!mStates.empty()) mStates.pop(); mTopicIndexBook.reset(); } void setVisible(bool newValue) override { WindowBase::setVisible(newValue); } void setBookMode() { mOptionsMode = false; mTopicsMode = false; setVisible(OptionsBTN, true); setVisible(OptionsOverlay, false); updateShowingPages(); updateCloseJournalButton(); } void setOptionsMode() { mOptionsMode = true; mTopicsMode = false; setVisible(OptionsBTN, false); setVisible(OptionsOverlay, true); 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); // 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) notifyQuests(getWidget(QuestsList)); else notifyTopics(getWidget(TopicsList)); } void pushBook(Book& book, unsigned int page) { DisplayState bs; bs.mPage = page; bs.mBook = book; mStates.push(bs); updateShowingPages(); updateCloseJournalButton(); } void replaceBook(Book& book, unsigned int page) { assert(!mStates.empty()); mStates.top().mBook = book; mStates.top().mPage = page; updateShowingPages(); } void popBook() { mStates.pop(); updateShowingPages(); updateCloseJournalButton(); } void updateCloseJournalButton() { setVisible(CloseBTN, mStates.size() < 2); setVisible(JournalBTN, mStates.size() >= 2); } void updateShowingPages() { Book book; unsigned int page; unsigned int relPages; if (!mStates.empty()) { book = mStates.top().mBook; page = mStates.top().mPage; relPages = book->pageCount() - page; } else { page = 0; relPages = 0; } MyGUI::Widget* nextPageBtn = getWidget(NextPageBTN); MyGUI::Widget* prevPageBtn = getWidget(PrevPageBTN); MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool nextPageVisible = relPages > 2; nextPageBtn->setVisible(nextPageVisible); bool prevPageVisible = page > 0; prevPageBtn->setVisible(prevPageVisible); if (focus == nextPageBtn && !nextPageVisible && prevPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(prevPageBtn); else if (focus == prevPageBtn && !prevPageVisible && nextPageVisible) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nextPageBtn); setVisible(PageOneNum, relPages > 0); setVisible(PageTwoNum, relPages > 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); } void notifyKeyPress(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::ArrowUp) notifyPrevPage(sender); else if (key == MyGUI::KeyCode::ArrowDown) notifyNextPage(sender); } void notifyTopicClicked(intptr_t linkId) { Book topicBook = createTopicBook(linkId); if (mStates.size() > 1) replaceBook(topicBook, 0); else pushBook(topicBook, 0); setVisible(OptionsOverlay, false); setVisible(OptionsBTN, true); setVisible(JournalBTN, true); mOptionsMode = false; mTopicsMode = false; MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } 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) { if (i->first == topic) topicId = intptr_t(&i->second); } notifyTopicClicked(topicId); } void notifyQuestClicked(const std::string& name, int id) { Book book = createQuestBook(name); if (mStates.size() > 1) replaceBook(book, 0); else pushBook(book, 0); setVisible(OptionsOverlay, false); setVisible(OptionsBTN, true); setVisible(JournalBTN, true); mOptionsMode = false; MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyOptions(MyGUI::Widget* _sender) { setOptionsMode(); if (!mTopicIndexBook) mTopicIndexBook = createTopicIndexBook(); if (mIndexPagesCount == 3) { 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); } } void notifyJournal(MyGUI::Widget* _sender) { assert(mStates.size() > 1); popBook(); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyIndexLinkClicked(MWGui::TypesetBook::InteractiveId index) { setVisible(LeftTopicIndex, false); setVisible(CenterTopicIndex, false); setVisible(RightTopicIndex, false); setVisible(TopicsList, true); mTopicsMode = true; Gui::MWList* list = getWidget(TopicsList); list->clear(); AddNamesToList add(list); mModel->visitTopicNamesStartingWith(index, add); list->adjustSize(); 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(ESM::RefId::stringRefId("book page")); } struct AddNamesToList { AddNamesToList(Gui::MWList* list) : mList(list) { } Gui::MWList* mList; void operator()(std::string_view name, bool finished = false) { mList->addItem(name); } }; struct SetNamesInactive { SetNamesInactive(Gui::MWList* list) : mList(list) { } Gui::MWList* mList; void operator()(std::string_view name, bool finished) { if (finished) { mList->getItemWidget(name)->setStateSelected(true); } } }; void notifyQuests(MyGUI::Widget* _sender) { mQuestMode = true; 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(); AddNamesToList add(list); mModel->visitQuestNames(!mAllQuests, add); list->sort(); list->adjustSize(); if (mAllQuests) { SetNamesInactive setInactive(list); mModel->visitQuestNames(false, setInactive); } MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } void notifyShowAll(MyGUI::Widget* _sender) { mAllQuests = true; notifyQuests(_sender); } void notifyShowActive(MyGUI::Widget* _sender) { mAllQuests = false; notifyQuests(_sender); } void notifyCancel(MyGUI::Widget* _sender) { if (mTopicsMode) { notifyTopics(_sender); } else { setBookMode(); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); } } void notifyClose(MyGUI::Widget* _sender) { MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); winMgr->playSound(ESM::RefId::stringRefId("book close")); winMgr->popGuiMode(); } void notifyMouseWheel(MyGUI::Widget* sender, int rel) { if (rel < 0) notifyNextPage(sender); else notifyPrevPage(sender); } void notifyNextPage(MyGUI::Widget* _sender) { if (mOptionsMode) return; if (!mStates.empty()) { unsigned int& page = mStates.top().mPage; Book book = mStates.top().mBook; if (page + 2 < book->pageCount()) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); page += 2; updateShowingPages(); } } } void notifyPrevPage(MyGUI::Widget* _sender) { if (mOptionsMode) return; if (!mStates.empty()) { unsigned int& page = mStates.top().mPage; if (page >= 2) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("book page")); page -= 2; updateShowingPages(); } } } }; } // glue the implementation to the interface std::unique_ptr MWGui::JournalWindow::create( JournalViewModel::Ptr Model, bool questList, ToUTF8::FromType encoding) { return std::make_unique(Model, questList, encoding); } MWGui::JournalWindow::JournalWindow() : BookWindowBase("openmw_journal.layout") { } openmw-openmw-0.49.0/apps/openmw/mwgui/journalwindow.hpp000066400000000000000000000015141503074453300234340ustar00rootroot00000000000000#ifndef MWGUI_JOURNAL_H #define MWGUI_JOURNAL_H #include "windowbase.hpp" #include #include namespace MWBase { class WindowManager; } namespace MWGui { struct JournalViewModel; struct JournalWindow : public BookWindowBase { JournalWindow(); /// construct a new instance of the one JournalWindow implementation static std::unique_ptr create( std::shared_ptr model, bool questList, ToUTF8::FromType encoding); /// destroy this instance of the JournalWindow implementation virtual ~JournalWindow() {} /// show/hide the journal window void setVisible(bool newValue) override = 0; std::string_view getWindowIdForLua() const override { return "Journal"; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/keyboardnavigation.cpp000066400000000000000000000226661503074453300244200ustar00rootroot00000000000000#include "keyboardnavigation.hpp" #include #include #include #include #include #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()) { MyGUI::Widget* w = enumerator.current(); if (!w->getVisible() || !w->getEnabled()) continue; if (w->getNeedKeyFocus() && shouldAcceptKeyFocus(w)) results.push_back(w); else getKeyFocusWidgets(w, results); } } 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) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void KeyboardNavigation::saveFocus(int mode) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (shouldAcceptKeyFocus(focus)) { mKeyFocus[mode] = focus; } else if (shouldAcceptKeyFocus(mCurrentFocus)) { mKeyFocus[mode] = mCurrentFocus; } } void KeyboardNavigation::restoreFocus(int mode) { 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); } } void KeyboardNavigation::_unlinkWidget(MyGUI::Widget* widget) { for (std::pair& w : mKeyFocus) if (w.second == widget) w.second = nullptr; if (widget == mCurrentFocus) mCurrentFocus = nullptr; } 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()) { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); return; } MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); 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; } } 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); } 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; } 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; 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::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; MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); 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->getNeedKeyFocus()) && isCycle) { // if nothing is selected, select the first widget return selectFirstWidget(); } if (!focus) return false; MyGUI::Widget* window = focus; while (window && window->getParent()) window = window->getParent(); MyGUI::VectorWidgetPtr keyFocusList; getKeyFocusWidgets(window, keyFocusList); if (keyFocusList.empty()) return false; MyGUI::VectorWidgetPtr::iterator found = std::find(keyFocusList.begin(), keyFocusList.end(), focus); if (found == keyFocusList.end()) { if (isCycle) return selectFirstWidget(); else return false; } bool forward = (direction == D_Next || direction == D_Right || direction == D_Down); 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); 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; MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(keyFocusList[index]); return true; } 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]); 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()) { focus->eventMouseButtonClick(focus); return true; } return false; } } openmw-openmw-0.49.0/apps/openmw/mwgui/keyboardnavigation.hpp000066400000000000000000000022571503074453300244170ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_KEYBOARDNAVIGATION_H #define OPENMW_MWGUI_KEYBOARDNAVIGATION_H #include #include namespace MWGui { class KeyboardNavigation : public MyGUI::IUnlinkWidget { public: KeyboardNavigation(); ~KeyboardNavigation(); /// @return Was the key handled by this class? bool injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat); void saveFocus(int mode); void restoreFocus(int mode); void _unlinkWidget(MyGUI::Widget* widget) override; void onFrame(); /// Set a key focus widget for this window, if one isn't already set. void setDefaultFocus(MyGUI::Widget* window, MyGUI::Widget* defaultFocus); void setModalWindow(MyGUI::Widget* window); void setEnabled(bool enabled); private: bool switchFocus(int direction, bool wrap); bool selectFirstWidget(); /// Send button press event to focused button bool accept(); std::map mKeyFocus; MyGUI::Widget* mCurrentFocus; MyGUI::Widget* mModalWindow; bool mEnabled; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/layout.cpp000066400000000000000000000043521503074453300220450ustar00rootroot00000000000000#include "layout.hpp" #include #include #include #include #include #include namespace MWGui { void Layout::initialise(std::string_view _layout) { const auto MAIN_WINDOW = "_Main"; mLayoutName = _layout; 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) { if (widget->getName() == main_name) mMainWidget = widget; // 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() { setVisible(false); MyGUI::Gui::getInstance().destroyWidget(mMainWidget); mListWindowRoot.clear(); } void Layout::setCoord(int x, int y, int w, int h) { mMainWidget->setCoord(x, y, w, h); } void Layout::setVisible(bool b) { mMainWidget->setVisible(b); } void Layout::setText(std::string_view name, std::string_view caption) { MyGUI::Widget* pt; getWidget(pt, name); static_cast(pt)->setCaption(MyGUI::UString(caption)); } void Layout::setTitle(std::string_view title) { MyGUI::Window* window = static_cast(mMainWidget); if (window->getCaption() != title) window->setCaptionWithReplacing(MyGUI::UString(title)); } MyGUI::Widget* Layout::getWidget(std::string_view _name) { std::string target = mPrefix; target += _name; for (MyGUI::Widget* widget : mListWindowRoot) { MyGUI::Widget* find = widget->findWidget(target); if (nullptr != find) { return find; } } MYGUI_EXCEPT("widget name '" << _name << "' in layout '" << mLayoutName << "' not found."); } } openmw-openmw-0.49.0/apps/openmw/mwgui/layout.hpp000066400000000000000000000037711503074453300220560ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_LAYOUT_H #define OPENMW_MWGUI_LAYOUT_H #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(std::string_view layout) : mMainWidget(nullptr) { initialise(layout); assert(mMainWidget); } virtual ~Layout() { try { shutdown(); } catch (const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } MyGUI::Widget* getWidget(std::string_view name); template void getWidget(T*& _widget, std::string_view _name) { 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; } private: void initialise(std::string_view layout); void shutdown(); public: void setCoord(int x, int y, int w, int h); virtual void setVisible(bool b); void setText(std::string_view name, std::string_view caption); // NOTE: this assume that mMainWidget is of type Window. void setTitle(std::string_view title); MyGUI::Widget* mMainWidget; protected: std::string mPrefix; std::string mLayoutName; MyGUI::VectorWidgetPtr mListWindowRoot; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/levelupdialog.cpp000066400000000000000000000322641503074453300233670ustar00rootroot00000000000000#include "levelupdialog.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.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 "../mwsound/constants.hpp" #include "class.hpp" namespace { constexpr unsigned int sMaxCoins = 3; constexpr int sColumnOffsets[] = { 32, 218 }; } namespace MWGui { LevelupDialog::LevelupDialog() : WindowBase("openmw_levelup_dialog.layout") , mCoinCount(sMaxCoins) { getWidget(mOkButton, "OkButton"); getWidget(mClassImage, "ClassImage"); getWidget(mLevelText, "LevelText"); getWidget(mLevelDescription, "LevelDescription"); getWidget(mCoinBox, "Coins"); getWidget(mAssignWidget, "AssignWidget"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &LevelupDialog::onOkButtonClicked); { 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 < sMaxCoins; ++i) { 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); } center(); } void LevelupDialog::setAttributeValues() { MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { int val = creatureStats.getAttribute(attribute.mId).getBase(); if (std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute.mId) != mSpentAttributes.end()) { val += pcStats.getLevelupAttributeMultiplier(attribute.mId); } if (val >= 100) val = 100; mAttributeWidgets[attribute.mId].mValue->setCaption(MyGUI::utility::toString(val)); } } void LevelupDialog::resetCoins() { constexpr int coinSpacing = 33; int curX = mCoinBox->getWidth() / 2 - (coinSpacing * (mCoinCount - 1) + 16 * mCoinCount) / 2; for (unsigned int i = 0; i < sMaxCoins; ++i) { MyGUI::ImageBox* image = mCoins[i]; image->detachFromWidget(); image->attachToWidget(mCoinBox); if (i < mCoinCount) { mCoins[i]->setVisible(true); image->setCoord(MyGUI::IntCoord(curX, 0, 16, 16)); curX += 16 + coinSpacing; } else mCoins[i]->setVisible(false); } } void LevelupDialog::assignCoins() { resetCoins(); for (size_t i = 0; i < mSpentAttributes.size(); ++i) { MyGUI::ImageBox* image = mCoins[i]; image->detachFromWidget(); image->attachToWidget(mAssignWidget); const auto& attribute = mSpentAttributes[i]; const auto& widgets = mAttributeWidgets[attribute]; const int xdiff = widgets.mMultiplier->getCaption().empty() ? 0 : 20; const auto* hbox = widgets.mButton->getParent(); MyGUI::IntPoint pos = hbox->getPosition(); pos.left -= 22 + xdiff; pos.top += (hbox->getHeight() - image->getHeight()) / 2; image->setPosition(pos); } setAttributeValues(); } void LevelupDialog::onOpen() { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); const MWMechanics::CreatureStats& creatureStats = player.getClass().getCreatureStats(player); const MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); 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; mLevelText->setCaptionWithReplacing("#{sLevelUpMenu1} " + MyGUI::utility::toString(level)); std::string_view levelupdescription; levelupdescription = Fallback::Map::getString("Level_Up_Level" + MyGUI::utility::toString(level)); if (levelupdescription.empty()) levelupdescription = Fallback::Map::getString("Level_Up_Default"); mLevelDescription->setCaption(MyGUI::UString(levelupdescription)); unsigned int availableAttributes = 0; for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { const auto& widgets = mAttributeWidgets[attribute.mId]; if (pcStats.getAttribute(attribute.mId).getBase() < 100) { widgets.mButton->setEnabled(true); widgets.mValue->setEnabled(true); availableAttributes++; 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 { widgets.mButton->setEnabled(false); widgets.mValue->setEnabled(false); widgets.mMultiplier->setCaption({}); } } mCoinCount = std::min(sMaxCoins, availableAttributes); mSpentAttributes.clear(); resetCoins(); setAttributeValues(); center(); // Play LevelUp Music 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); if (mSpentAttributes.size() < mCoinCount) MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage36}"); else { // increase attributes for (unsigned int i = 0; i < mCoinCount; ++i) { MWMechanics::AttributeValue attribute = pcStats.getAttribute(mSpentAttributes[i]); attribute.setBase(attribute.getBase() + pcStats.getLevelupAttributeMultiplier(mSpentAttributes[i])); if (attribute.getBase() >= 100) attribute.setBase(100); pcStats.setAttribute(mSpentAttributes[i], attribute); } pcStats.levelUp(); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Levelup); } } void LevelupDialog::onAttributeClicked(MyGUI::Widget* sender) { auto attribute = *sender->getUserData(); auto found = std::find(mSpentAttributes.begin(), mSpentAttributes.end(), attribute); if (found != mSpentAttributes.end()) mSpentAttributes.erase(found); else { if (mSpentAttributes.size() == mCoinCount) mSpentAttributes[mCoinCount - 1] = attribute; else mSpentAttributes.push_back(attribute); } assignCoins(); } std::string_view LevelupDialog::getLevelupClassImage( const int combatIncreases, const int magicIncreases, const int stealthIncreases) { std::string_view ret = "acrobat"; int total = combatIncreases + magicIncreases + stealthIncreases; if (total == 0) return ret; int combatFraction = static_cast(static_cast(combatIncreases) / total * 10.f); int magicFraction = static_cast(static_cast(magicIncreases) / total * 10.f); int stealthFraction = static_cast(static_cast(stealthIncreases) / total * 10.f); if (combatFraction > 7) ret = "warrior"; else if (magicFraction > 7) ret = "mage"; else if (stealthFraction > 7) ret = "thief"; switch (combatFraction) { case 7: ret = "warrior"; break; case 6: if (stealthFraction == 1) ret = "barbarian"; else if (stealthFraction == 3) ret = "crusader"; else ret = "knight"; break; case 5: if (stealthFraction == 3) ret = "scout"; else ret = "archer"; break; case 4: ret = "rogue"; break; default: break; } switch (magicFraction) { case 7: ret = "mage"; break; case 6: if (combatFraction == 2) ret = "sorcerer"; else if (combatIncreases == 3) ret = "healer"; else ret = "battlemage"; break; case 5: ret = "witchhunter"; break; case 4: ret = "spellsword"; // In vanilla there's also code for "nightblade", however it seems to be unreachable. break; default: break; } switch (stealthFraction) { case 7: ret = "thief"; break; case 6: if (magicFraction == 1) ret = "agent"; else if (magicIncreases == 3) ret = "assassin"; else ret = "acrobat"; break; case 5: if (magicIncreases == 3) ret = "monk"; else ret = "pilgrim"; break; case 3: if (magicFraction == 3) ret = "bard"; break; default: break; } return ret; } } openmw-openmw-0.49.0/apps/openmw/mwgui/levelupdialog.hpp000066400000000000000000000024711503074453300233710ustar00rootroot00000000000000#ifndef MWGUI_LEVELUPDIALOG_H #define MWGUI_LEVELUPDIALOG_H #include #include "windowbase.hpp" namespace MWGui { class LevelupDialog : public WindowBase { public: LevelupDialog(); 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::ScrollView* mAssignWidget; std::map mAttributeWidgets; std::vector mCoins; std::vector mSpentAttributes; unsigned int mCoinCount; void onOkButtonClicked(MyGUI::Widget* sender); void onAttributeClicked(MyGUI::Widget* sender); void assignCoins(); void resetCoins(); void setAttributeValues(); std::string_view getLevelupClassImage( const int combatIncreases, const int magicIncreases, const int stealthIncreases); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/loadingscreen.cpp000066400000000000000000000313611503074453300233450ustar00rootroot00000000000000#include "loadingscreen.hpp" #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 "backgroundimage.hpp" namespace MWGui { LoadingScreen::LoadingScreen(Resource::ResourceSystem* resourceSystem, osgViewer::Viewer* viewer) : WindowBase("openmw_loading_screen.layout") , mResourceSystem(resourceSystem) , mViewer(viewer) , mTargetFrameRate(120.0) , mLastWallpaperChangeTime(0.0) , mLastRenderTime(0.0) , mLoadingOnTime(0.0) , mImportantLabel(false) , mNestedLoadingCount(0) , mProgress(0) , mShowWallpaper(true) { getWidget(mLoadingText, "LoadingText"); getWidget(mProgressBar, "ProgressBar"); getWidget(mLoadingBox, "LoadingBox"); getWidget(mSceneImage, "Scene"); getWidget(mSplashImage, "Splash"); mProgressBar->setScrollViewPage(1); findSplashScreens(); } LoadingScreen::~LoadingScreen() {} void LoadingScreen::findSplashScreens() { 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)) { 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) { mImportantLabel = important; mLoadingText->setCaptionWithReplacing(label); int padding = mLoadingBox->getWidth() - mLoadingText->getWidth(); 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); else mLoadingBox->setPosition(mMainWidget->getWidth() / 2 - mLoadingBox->getWidth() / 2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8); } void LoadingScreen::setVisible(bool visible) { WindowBase::setVisible(visible); mSplashImage->setVisible(visible); mSceneImage->setVisible(visible); } double LoadingScreen::getTargetFrameRate() const { double frameRateLimit = MWBase::Environment::get().getFrameRateLimit(); if (frameRateLimit > 0) return std::min(frameRateLimit, mTargetFrameRate); else return mTargetFrameRate; } class CopyFramebufferToTextureCallback : public osg::Camera::DrawCallback { public: CopyFramebufferToTextureCallback(osg::Texture2D* texture) : mOneshot(true) , mTexture(texture) { } void operator()(osg::RenderInfo& renderInfo) const override { int w = renderInfo.getCurrentCamera()->getViewport()->width(); int h = renderInfo.getCurrentCamera()->getViewport()->height(); mTexture->copyTexImage2D(*renderInfo.getState(), 0, 0, w, h); mOneshot = false; } void reset() { mOneshot = true; } private: mutable bool mOneshot; osg::ref_ptr mTexture; }; class DontComputeBoundCallback : public osg::Node::ComputeBoundingSphereCallback { public: osg::BoundingSphere computeBound(const osg::Node&) const override { return osg::BoundingSphere(); } }; void LoadingScreen::loadingOn() { // Early-out if already on if (mNestedLoadingCount++ > 0 && mMainWidget->getVisible()) return; 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() mViewer->getSceneData()->setComputeBoundingSphereCallback(new DontComputeBoundCallback); if (const osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { mOldIcoMin = ico->getMinimumTimeAvailableForGLCompileAndDeletePerFrame(); mOldIcoMax = ico->getMaximumNumOfObjectsToCompilePerFrame(); } setVisible(true); mShowWallpaper = MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; if (mShowWallpaper) { changeWallpaper(); } MWBase::Environment::get().getWindowManager()->pushGuiMode(mShowWallpaper ? GM_LoadingWallpaper : GM_Loading); } void LoadingScreen::loadingOff() { if (--mNestedLoadingCount > 0) return; if (mLastRenderTime < mLoadingOnTime) { // the loading was so fast that we didn't show loading screen at all // we may still want to show the label if the caller requested it if (mImportantLabel) { MWBase::Environment::get().getWindowManager()->messageBox(mLoadingText->getCaption()); mImportantLabel = false; } } else mImportantLabel = false; // label was already shown on loading screen mViewer->getSceneData()->setComputeBoundingSphereCallback(nullptr); mViewer->getSceneData()->dirtyBound(); setVisible(false); if (osgUtil::IncrementalCompileOperation* ico = mViewer->getIncrementalCompileOperation()) { ico->setMinimumTimeAvailableForGLCompileAndDeletePerFrame(mOldIcoMin); ico->setMaximumNumOfObjectsToCompilePerFrame(mOldIcoMax); } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Loading); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_LoadingWallpaper); } void LoadingScreen::changeWallpaper() { if (!mSplashScreens.empty()) { 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 mSplashImage->setVisible(true); mSplashImage->setBackgroundImage(randomSplash, true, Settings::gui().mStretchMenuBackground); } mSceneImage->setBackgroundImage({}); mSceneImage->setVisible(false); } void LoadingScreen::setProgressRange(size_t range) { mProgressBar->setScrollRange(range + 1); mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize(0); mProgress = 0; } 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()) return; value = std::min(value, mProgressBar->getScrollRange() - 1); mProgress = value; mProgressBar->setScrollPosition(0); mProgressBar->setTrackSize( static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } void LoadingScreen::increaseProgress(size_t increase) { mProgressBar->setScrollPosition(0); size_t value = mProgress + increase; value = std::min(value, mProgressBar->getScrollRange() - 1); mProgress = value; mProgressBar->setTrackSize( static_cast(value / (float)(mProgressBar->getScrollRange()) * mProgressBar->getLineSize())); draw(); } bool LoadingScreen::needToDrawLoadingScreen() { if (mTimer.time_m() <= mLastRenderTime + (1.0 / getTargetFrameRate()) * 1000.0) return false; // the minimal delay before a loading screen shows const float initialDelay = 0.05; bool alreadyShown = (mLastRenderTime > mLoadingOnTime); float diff = (mTimer.time_m() - mLoadingOnTime); if (!alreadyShown) { // bump the delay by the current progress - i.e. if during the initial delay the loading // has almost finished, no point showing the loading screen now diff -= mProgress / static_cast(mProgressBar->getScrollRange()) * 100.f; } if (!mShowWallpaper && diff < initialDelay * 1000) return false; return true; } void LoadingScreen::setupCopyFramebufferToTextureCallback() { // Copy the current framebuffer onto a texture and display that texture as the background image // Note, we could also set the camera to disable clearing and have the background image transparent, // but then we get shaking effects on buffer swaps. 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 = std::make_unique(mTexture); } if (!mCopyFramebufferToTextureCallback) { mCopyFramebufferToTextureCallback = new CopyFramebufferToTextureCallback(mTexture); } mViewer->getCamera()->removeInitialDrawCallback(mCopyFramebufferToTextureCallback); mViewer->getCamera()->addInitialDrawCallback(mCopyFramebufferToTextureCallback); mCopyFramebufferToTextureCallback->reset(); mSplashImage->setBackgroundImage({}); mSplashImage->setVisible(false); mSceneImage->setRenderItemTexture(mGuiTexture.get()); mSceneImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); mSceneImage->setVisible(true); } void LoadingScreen::draw() { if (!needToDrawLoadingScreen()) return; if (mShowWallpaper && mTimer.time_m() > mLastWallpaperChangeTime + 5000 * 1) { mLastWallpaperChangeTime = mTimer.time_m(); changeWallpaper(); } if (!mShowWallpaper && mLastRenderTime < mLoadingOnTime) { setupCopyFramebufferToTextureCallback(); } MWBase::Environment::get().getInputManager()->update(0, true, true); 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->setMaximumNumOfObjectsToCompilePerFrame(1000); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); mLastRenderTime = mTimer.time_m(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/loadingscreen.hpp000066400000000000000000000043311503074453300233470ustar00rootroot00000000000000#ifndef MWGUI_LOADINGSCREEN_H #define MWGUI_LOADINGSCREEN_H #include #include #include #include "windowbase.hpp" #include namespace osgViewer { class Viewer; } namespace osg { class Texture2D; } namespace Resource { class ResourceSystem; } namespace MWGui { class BackgroundImage; class CopyFramebufferToTextureCallback; class LoadingScreen : public WindowBase, public Loading::Listener { public: LoadingScreen(Resource::ResourceSystem* resourceSystem, osgViewer::Viewer* viewer); 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() override; void loadingOff() 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; double getTargetFrameRate() const; private: void findSplashScreens(); bool needToDrawLoadingScreen(); void setupCopyFramebufferToTextureCallback(); Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mViewer; double mTargetFrameRate; double mLastWallpaperChangeTime; double mLastRenderTime; osg::Timer mTimer; double mLoadingOnTime; bool mImportantLabel; int mNestedLoadingCount; size_t mProgress; bool mShowWallpaper; float mOldIcoMin = 0.f; unsigned int mOldIcoMax = 0; MyGUI::Widget* mLoadingBox; MyGUI::TextBox* mLoadingText; MyGUI::ScrollBar* mProgressBar; BackgroundImage* mSplashImage; BackgroundImage* mSceneImage; std::vector mSplashScreens; osg::ref_ptr mTexture; osg::ref_ptr mCopyFramebufferToTextureCallback; std::unique_ptr mGuiTexture; void changeWallpaper(); void draw(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/mainmenu.cpp000066400000000000000000000304271503074453300223430ustar00rootroot00000000000000#include "mainmenu.hpp" #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/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()); const MWBase::WindowManager& windowManager = *MWBase::Environment::get().getWindowManager(); bool paused = false; while (mRunning) { if (windowManager.isWindowVisible()) { if (paused) { mVideo->resume(); paused = false; } // If finished playing, start again if (!mVideo->update()) mVideo->playVideo("video\\menu_background.bik"); } else if (!paused) { paused = true; mVideo->pause(); } 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) , mBackground(nullptr) { getWidget(mVersionText, "VersionText"); mVersionText->setCaption(versionDescription); constexpr VFS::Path::NormalizedView menuBackgroundVideo("video/menu_background.bik"); mHasAnimatedMenu = mVFS->exists(menuBackgroundVideo); updateMenu(); } void MainMenu::onResChange(int w, int h) { mWidth = w; mHeight = h; updateMenu(); if (mVideo) mVideo->resize(w, h); } 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; showBackground(isMainMenu); if (visible) { if (isMainMenu) { if (mButtons["loadgame"]->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["loadgame"]); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["newgame"]); } else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mButtons["return"]); } Layout::setVisible(visible); } void MainMenu::onNewGameConfirmed() { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); MWBase::Environment::get().getStateManager()->newGame(); } void MainMenu::onExitConfirmed() { MWBase::Environment::get().getStateManager()->requestQuit(); } void MainMenu::onButtonClicked(MyGUI::Widget* sender) { MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); const std::string& name = *sender->getUserData(); winMgr->playSound(ESM::RefId::stringRefId("Menu Click")); if (name == "return") { winMgr->removeGuiMode(GM_MainMenu); } else if (name == "credits") winMgr->playVideo("mw_credits.bik", true); else if (name == "exitgame") { if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) onExitConfirmed(); else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:QuitGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onExitConfirmed); dialog->eventCancelClicked.clear(); } } else if (name == "newgame") { if (MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame) onNewGameConfirmed(); else { ConfirmationDialog* dialog = winMgr->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:NewGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &MainMenu::onNewGameConfirmed); dialog->eventCancelClicked.clear(); } } else if (name == "loadgame" || name == "savegame") { if (!mSaveGameDialog) 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) { mVideo.reset(); } if (mBackground && !show) { MyGUI::Gui::getInstance().destroyWidget(mBackground); mBackground = nullptr; } if (!show) return; const bool stretch = Settings::gui().mStretchMenuBackground; if (mHasAnimatedMenu) { if (!mVideo) mVideo.emplace(mVFS); const auto& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); int screenWidth = viewSize.width; int screenHeight = viewSize.height; mVideo->resize(screenWidth, screenHeight); } else { if (!mBackground) { 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); } } bool MainMenu::exit() { if (MWBase::Environment::get().getWindowManager()->isSettingsWindowVisible()) { MWBase::Environment::get().getWindowManager()->toggleSettingsWindow(); return false; } return MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_Running; } void MainMenu::updateMenu() { setCoord(0, 0, mWidth, mHeight); if (!mButtonBox) mButtonBox = mMainWidget->createWidget({}, MyGUI::IntCoord(0, 0, 0, 0), MyGUI::Align::Default); int curH = 0; MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); mVersionText->setVisible(state == MWBase::StateManager::State_NoGame); std::vector buttons; 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(MWWorld::Globals::sCharGenState) == -1 && MWBase::Environment::get().getWindowManager()->isSavingAllowed()) buttons.emplace_back("savegame"); 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) buttons.emplace_back("credits"); buttons.emplace_back("exitgame"); // Create new buttons if needed for (std::string_view id : { "return", "newgame", "savegame", "loadgame", "options", "credits", "exitgame" }) { if (mButtons.find(id) == mButtons.end()) { 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(buttonId); } } // Start by hiding all buttons int maxwidth = 0; for (const auto& buttonPair : mButtons) { buttonPair.second->setVisible(false); MyGUI::IntSize requested = buttonPair.second->getRequestedSize(); if (requested.width > maxwidth) maxwidth = requested.width; } // Now show and position the ones we want for (const std::string& buttonId : buttons) { 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. // If they have a different resolution, scale them. MyGUI::IntSize requested = button->getRequestedSize(); float scale = requested.height / 64.f; button->setImageCoord(MyGUI::IntCoord(0, 0, requested.width, requested.height)); // 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; } 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); } else mButtonBox->setCoord(mWidth / 2 - maxwidth / 2, mHeight / 2 - curH / 2, maxwidth, curH); } } openmw-openmw-0.49.0/apps/openmw/mwgui/mainmenu.hpp000066400000000000000000000030251503074453300223420ustar00rootroot00000000000000#ifndef OPENMW_GAME_MWGUI_MAINMENU_H #define OPENMW_GAME_MWGUI_MAINMENU_H #include #include #include #include "savegamedialog.hpp" #include "windowbase.hpp" namespace Gui { class ImageButton; } namespace VFS { class Manager; } namespace MWGui { class BackgroundImage; class VideoWidget; class MenuVideo { MyGUI::ImageBox* mVideoBackground; VideoWidget* mVideo; std::thread mThread; bool mRunning; void run(); public: MenuVideo(const VFS::Manager* vfs); void resize(int w, int h); ~MenuVideo(); }; class MainMenu : public WindowBase { int mWidth; int mHeight; bool mHasAnimatedMenu; public: MainMenu(int w, int h, const VFS::Manager* vfs, const std::string& versionDescription); void onResChange(int w, int h) override; void setVisible(bool visible) override; bool exit() override; private: const VFS::Manager* mVFS; MyGUI::Widget* mButtonBox; MyGUI::TextBox* mVersionText; BackgroundImage* mBackground; std::optional mVideo; // For animated main menus std::map> mButtons; void onButtonClicked(MyGUI::Widget* sender); void onNewGameConfirmed(); void onExitConfirmed(); void showBackground(bool show); void updateMenu(); std::unique_ptr mSaveGameDialog; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/mapwindow.cpp000066400000000000000000001504501503074453300225360ustar00rootroot00000000000000#include "mapwindow.hpp" #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 "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwworld/worldmodel.hpp" #include "../mwrender/globalmap.hpp" #include "../mwrender/localmap.hpp" #include "confirmationdialog.hpp" #include namespace { const int cellSize = Constants::CellSizeInUnits; constexpr float speed = 1.08f; // the zoom speed, it should be greater than 1 enum LocalMapWidgetDepth { Local_MarkerAboveFogLayer = 0, Local_CompassLayer = 1, Local_FogLayer = 2, Local_MarkerLayer = 3, Local_MapLayer = 4 }; enum GlobalMapWidgetDepth { Global_CompassLayer = 0, Global_MarkerLayer = 1, Global_ExploreOverlayLayer = 2, Global_MapLayer = 3 }; /// @brief A widget that changes its color when hovered. class MarkerWidget final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MarkerWidget) public: void setNormalColour(const MyGUI::Colour& colour) { mNormalColour = colour; setColour(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 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(); } void setCanvasSize(MyGUI::ScrollView* scrollView, const MyGUI::IntRect& grid, int widgetSize) { scrollView->setCanvasSize(widgetSize * (grid.width() + 1), widgetSize * (grid.height() + 1)); } } namespace MWGui { 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) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { mMarkers.erase(it); eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to delete"); } void CustomMarkerCollection::updateMarker(const ESM::CustomMarker& marker, const std::string& newNote) { std::pair range = mMarkers.equal_range(marker.mCell); for (ContainerType::iterator it = range.first; it != range.second; ++it) { if (it->second == marker) { it->second.mNote = newNote; eventMarkersChanged(); return; } } throw std::runtime_error("can't find marker to update"); } void CustomMarkerCollection::clear() { mMarkers.clear(); eventMarkersChanged(); } CustomMarkerCollection::ContainerType::const_iterator CustomMarkerCollection::begin() const { return mMarkers.begin(); } CustomMarkerCollection::ContainerType::const_iterator CustomMarkerCollection::end() const { return mMarkers.end(); } CustomMarkerCollection::RangeType CustomMarkerCollection::getMarkers(const ESM::RefId& cellId) const { return mMarkers.equal_range(cellId); } size_t CustomMarkerCollection::size() const { return mMarkers.size(); } // ------------------------------------------------------ LocalMapBase::LocalMapBase( CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled) : mLocalMapRender(localMapRender) , mFogOfWarEnabled(fogOfWarEnabled) , mCustomMarkers(markers) { mCustomMarkers.eventMarkersChanged += MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } LocalMapBase::~LocalMapBase() { mCustomMarkers.eventMarkersChanged -= MyGUI::newDelegate(this, &LocalMapBase::updateCustomMarkers); } MWGui::LocalMapBase::MapEntry& LocalMapBase::addMapEntry() { const int mapWidgetSize = Settings::map().mLocalMapWidgetSize; MyGUI::ImageBox* map = mLocalMap->createWidget( "ImageBox", MyGUI::IntCoord(0, 0, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); map->setDepth(Local_MapLayer); MyGUI::ImageBox* fog = mLocalMap->createWidget( "ImageBox", MyGUI::IntCoord(0, 0, mapWidgetSize, mapWidgetSize), MyGUI::Align::Top | MyGUI::Align::Left); fog->setDepth(Local_FogLayer); fog->setColour(MyGUI::Colour(0, 0, 0)); map->setNeedMouseFocus(false); fog->setNeedMouseFocus(false); return mMaps.emplace_back(map, fog); } void LocalMapBase::init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance) { mLocalMap = widget; mCompass = compass; mGrid = createRect({ 0, 0 }, cellDistance); mExtCellDistance = cellDistance; const int mapWidgetSize = Settings::map().mLocalMapWidgetSize; setCanvasSize(mLocalMap, mGrid, mapWidgetSize); mCompass->setDepth(Local_CompassLayer); mCompass->setNeedMouseFocus(false); int numCells = (mGrid.width() + 1) * (mGrid.height() + 1); for (int i = 0; i < numCells; ++i) addMapEntry(); } bool LocalMapBase::toggleFogOfWar() { mFogOfWarToggled = !mFogOfWarToggled; applyFogOfWar(); return mFogOfWarToggled; } void LocalMapBase::applyFogOfWar() { if (!mFogOfWarToggled || !mFogOfWarEnabled) { for (auto& entry : mMaps) { entry.mFogWidget->setImageTexture({}); entry.mFogTexture.reset(); } } redraw(); } MyGUI::IntPoint LocalMapBase::getPosition(int cellX, int cellY, float nX, float nY) const { // normalized cell coordinates auto mapWidgetSize = getWidgetSize(); return MyGUI::IntPoint(std::round((nX + cellX - mGrid.left) * mapWidgetSize), std::round((nY - cellY + mGrid.bottom) * mapWidgetSize)); } MyGUI::IntPoint LocalMapBase::getMarkerPosition(float worldX, float worldY, MarkerUserData& markerPos) const { osg::Vec2i cellIndex; // normalized cell coordinates float nX, nY; if (mActiveCell->isExterior()) { 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 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 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() { for (MyGUI::Widget* widget : mCustomMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mCustomMarkerWidgets.clear(); if (!mActiveCell) return; auto updateMarkers = [this](CustomMarkerCollection::RangeType markers) { for (auto it = markers.first; it != markers.second; ++it) { const ESM::CustomMarker& marker = it->second; MarkerUserData markerPos(mLocalMapRender); MarkerWidget* markerWidget = mLocalMap->createWidget("CustomMarkerButton", getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setUserString("ToolTipType", "Layout"); markerWidget->setUserString("ToolTipLayout", "TextToolTipOneLine"); markerWidget->setUserString("Caption_TextOneLine", MyGUI::TextIterator::toTagsString(marker.mNote)); markerWidget->setNormalColour(MyGUI::Colour(0.6f, 0.6f, 0.6f)); markerWidget->setHoverColour(MyGUI::Colour(1.0f, 1.0f, 1.0f)); markerWidget->setUserData(marker); markerWidget->setNeedMouseFocus(true); customMarkerCreated(markerWidget); mCustomMarkerWidgets.push_back(markerWidget); } }; if (mActiveCell->isExterior()) { for (int x = mGrid.left; x <= mGrid.right; ++x) { for (int y = mGrid.top; y <= mGrid.bottom; ++y) { ESM::RefId cellRefId = getCellIdInWorldSpace(*mActiveCell, x, y); updateMarkers(mCustomMarkers.getMarkers(cellRefId)); } } } else updateMarkers(mCustomMarkers.getMarkers(mActiveCell->getId())); redraw(); } void LocalMapBase::setActiveCell(const MWWorld::Cell& cell) { if (&cell == mActiveCell) return; // don't do anything if we're still in the same cell const int x = cell.getGridX(); const int y = cell.getGridY(); MyGUI::IntSize oldSize{ mGrid.width(), mGrid.height() }; if (cell.isExterior()) { mGrid = createRect({ x, y }, mExtCellDistance); const MyGUI::IntRect activeGrid = createRect({ x, y }, Constants::CellGridRadius); mExteriorDoorMarkerWidgets.clear(); for (auto& [coord, doors] : mExteriorDoorsByCell) { if (!mHasALastActiveCell || !mGrid.inside({ coord.first, coord.second }) || activeGrid.inside({ coord.first, coord.second })) { mDoorMarkersToRecycle.insert(mDoorMarkersToRecycle.end(), doors.begin(), doors.end()); doors.clear(); } else mExteriorDoorMarkerWidgets.insert(mExteriorDoorMarkerWidgets.end(), doors.begin(), doors.end()); } for (auto& widget : mDoorMarkersToRecycle) widget->setVisible(false); if (mHasALastActiveCell) { for (const auto& entry : mMaps) { if (!mGrid.inside({ entry.mCellX, entry.mCellY })) mLocalMapRender->removeExteriorCell(entry.mCellX, entry.mCellY); } } } else mGrid = mLocalMapRender->getInteriorGrid(); mActiveCell = &cell; constexpr auto resetEntry = [](MapEntry& entry, bool visible, const MyGUI::IntPoint* position) { entry.mMapWidget->setVisible(visible); entry.mFogWidget->setVisible(visible); if (position) { entry.mMapWidget->setPosition(*position); entry.mFogWidget->setPosition(*position); } entry.mMapWidget->setRenderItemTexture(nullptr); entry.mFogWidget->setRenderItemTexture(nullptr); entry.mMapTexture.reset(); entry.mFogTexture.reset(); }; std::size_t usedEntries = 0; for (int cx = mGrid.left; cx <= mGrid.right; ++cx) { for (int cy = mGrid.top; cy <= mGrid.bottom; ++cy) { MapEntry& entry = usedEntries < mMaps.size() ? mMaps[usedEntries] : addMapEntry(); entry.mCellX = cx; entry.mCellY = cy; MyGUI::IntPoint position = getPosition(cx, cy, 0, 0); resetEntry(entry, true, &position); ++usedEntries; } } for (std::size_t i = usedEntries; i < mMaps.size(); ++i) { resetEntry(mMaps[i], false, nullptr); } if (oldSize != MyGUI::IntSize{ mGrid.width(), mGrid.height() }) setCanvasSize(mLocalMap, mGrid, getWidgetSize()); // Delay the door markers update until scripts have been given a chance to run. // 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) { mLocalMapRender->requestMap(cell); } void LocalMapBase::redraw() { // Redraw children in proper order 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 = getPosition(cellX, cellY, nx, ny) - MyGUI::IntPoint{ 16, 16 }; if (pos != mCompass->getPosition()) { notifyPlayerUpdate(); mCompass->setPosition(pos); } osg::Vec2f curPos((cellX + nx) * cellSize, (cellY + 1 - ny) * cellSize); if ((curPos - mCurPos).length2() > 0.001) { mCurPos = curPos; centerView(); } } void LocalMapBase::setPlayerDir(const float x, const float y) { if (x == mLastDirectionX && y == mLastDirectionY) return; notifyPlayerUpdate(); MyGUI::ISubWidget* main = mCompass->getSubWidgetMain(); MyGUI::RotatingSkin* rotatingSubskin = main->castType(); rotatingSubskin->setCenter(MyGUI::IntPoint(16, 16)); float angle = std::atan2(x, y); rotatingSubskin->setAngle(angle); mLastDirectionX = x; mLastDirectionY = y; } void LocalMapBase::addDetectionMarkers(int type) { std::vector markers; MWBase::World* world = MWBase::Environment::get().getWorld(); world->listDetectedReferences(world->getPlayerPtr(), markers, MWBase::World::DetectionType(type)); if (markers.empty()) return; std::string_view markerTexture; if (type == MWBase::World::Detect_Creature) { markerTexture = "textures\\detect_animal_icon.dds"; } if (type == MWBase::World::Detect_Key) { markerTexture = "textures\\detect_key_icon.dds"; } if (type == MWBase::World::Detect_Enchantment) { markerTexture = "textures\\detect_enchantment_icon.dds"; } for (const MWWorld::Ptr& ptr : markers) { const ESM::Position& worldPos = ptr.getRefData().getPosition(); MarkerUserData markerPos(mLocalMapRender); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", getMarkerCoordinates(worldPos.pos[0], worldPos.pos[1], markerPos, 8), MyGUI::Align::Default); markerWidget->setDepth(Local_MarkerAboveFogLayer); markerWidget->setImageTexture(markerTexture); markerWidget->setImageCoord(MyGUI::IntCoord(0, 0, 8, 8)); markerWidget->setNeedMouseFocus(false); markerWidget->setUserData(markerPos); mMagicMarkerWidgets.push_back(markerWidget); } } void LocalMapBase::onFrame(float dt) { if (mNeedDoorMarkersUpdate) { updateDoorMarkers(); mNeedDoorMarkersUpdate = false; } mMarkerUpdateTimer += dt; if (mMarkerUpdateTimer >= 0.25) { mMarkerUpdateTimer = 0; updateMagicMarkers(); } updateRequiredMaps(); } bool widgetCropped(MyGUI::Widget* widget, MyGUI::Widget* cropTo) { MyGUI::IntRect coord = widget->getAbsoluteRect(); MyGUI::IntRect croppedCoord = cropTo->getAbsoluteRect(); return !coord.intersect(croppedCoord); } void LocalMapBase::updateRequiredMaps() { bool needRedraw = false; for (MapEntry& entry : mMaps) { if (widgetCropped(entry.mMapWidget, mLocalMap)) continue; if (!entry.mMapTexture) { 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 = std::make_unique(texture); entry.mMapWidget->setRenderItemTexture(entry.mMapTexture.get()); entry.mMapWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); needRedraw = true; } else entry.mMapTexture = std::make_unique(std::string(), nullptr); } if (!entry.mFogTexture && mFogOfWarToggled && mFogOfWarEnabled) { osg::ref_ptr tex = mLocalMapRender->getFogOfWarTexture(entry.mCellX, entry.mCellY); if (tex) { entry.mFogTexture = std::make_unique(tex); entry.mFogWidget->setRenderItemTexture(entry.mFogTexture.get()); entry.mFogWidget->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } else { entry.mFogWidget->setImageTexture("black"); entry.mFogTexture = std::make_unique(std::string(), nullptr); } needRedraw = true; } } if (needRedraw) redraw(); } void LocalMapBase::updateDoorMarkers() { std::vector doors; MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::WorldModel* worldModel = MWBase::Environment::get().getWorldModel(); mDoorMarkersToRecycle.insert( mDoorMarkersToRecycle.end(), mInteriorDoorMarkerWidgets.begin(), mInteriorDoorMarkerWidgets.end()); mInteriorDoorMarkerWidgets.clear(); if (!mActiveCell->isExterior()) { for (MyGUI::Widget* widget : mExteriorDoorMarkerWidgets) widget->setVisible(false); MWWorld::CellStore& cell = worldModel->getInterior(mActiveCell->getNameId()); world->getDoorMarkers(cell, doors); } else { for (MapEntry& entry : mMaps) { 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 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) destNotes.push_back(iter->second.mNote); 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); } 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() { // clear all previous markers for (MyGUI::Widget* widget : mMagicMarkerWidgets) MyGUI::Gui::getInstance().destroyWidget(widget); mMagicMarkerWidgets.clear(); addDetectionMarkers(MWBase::World::Detect_Creature); addDetectionMarkers(MWBase::World::Detect_Key); addDetectionMarkers(MWBase::World::Detect_Enchantment); // Add marker for the spot marked with Mark magic effect MWWorld::CellStore* markedCell = nullptr; ESM::Position markedPosition; MWBase::Environment::get().getWorld()->getPlayer().getMarkedPosition(markedCell, markedPosition); if (markedCell && markedCell->getCell()->getWorldSpace() == mActiveCell->getWorldSpace()) { MarkerUserData markerPos(mLocalMapRender); MyGUI::ImageBox* markerWidget = mLocalMap->createWidget("ImageBox", 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(); setCanvasSize(mLocalMap, mGrid, getWidgetSize()); const auto size = MyGUI::IntSize(std::ceil(mapWidgetSize), std::ceil(mapWidgetSize)); for (auto& entry : mMaps) { const auto position = getPosition(entry.mCellX, entry.mCellY, 0, 0); entry.mMapWidget->setCoord({ position, size }); entry.mFogWidget->setCoord({ position, size }); } MarkerUserData markerPos(mLocalMapRender); for (MyGUI::Widget* widget : currentDoorMarkersWidgets()) widget->setCoord(getMarkerCoordinates(widget, 8)); for (MyGUI::Widget* widget : mCustomMarkerWidgets) { const auto& marker = *widget->getUserData(); widget->setCoord(getMarkerCoordinates(marker.mWorldX, marker.mWorldY, markerPos, 16)); } for (MyGUI::Widget* widget : mMagicMarkerWidgets) widget->setCoord(getMarkerCoordinates(widget, 8)); } // ------------------------------------------------------------------------------------------ MapWindow::MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue) #ifdef USE_OPENXR : WindowPinnableBase("openmw_map_window_vr.layout") #else : WindowPinnableBase("openmw_map_window.layout") #endif , LocalMapBase(customMarkers, localMapRender, true) , NoDrop(drag, mMainWidget) , mGlobalMap(nullptr) , mGlobalMapImage(nullptr) , mGlobalMapOverlay(nullptr) , mEventBoxGlobal(nullptr) , mEventBoxLocal(nullptr) , mGlobalMapRender(std::make_unique(localMapRender->getRoot(), workQueue)) , mEditNoteDialog() { static bool registered = false; if (!registered) { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); registered = true; } mEditNoteDialog.setVisible(false); mEditNoteDialog.eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditOk); mEditNoteDialog.eventDeleteClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDelete); setCoord(500, 0, 320, 300); getWidget(mLocalMap, "LocalMap"); getWidget(mGlobalMap, "GlobalMap"); getWidget(mGlobalMapImage, "GlobalMapImage"); getWidget(mGlobalMapOverlay, "GlobalMapOverlay"); getWidget(mPlayerArrowLocal, "CompassLocal"); getWidget(mPlayerArrowGlobal, "CompassGlobal"); mPlayerArrowGlobal->setDepth(Global_CompassLayer); mPlayerArrowGlobal->setNeedMouseFocus(false); mGlobalMapImage->setDepth(Global_MapLayer); mGlobalMapOverlay->setDepth(Global_ExploreOverlayLayer); mLastScrollWindowCoordinates = mLocalMap->getCoord(); mLocalMap->eventChangeCoord += MyGUI::newDelegate(this, &MapWindow::onChangeScrollWindowCoord); mGlobalMap->setVisible(false); getWidget(mButton, "WorldButton"); mButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MapWindow::onWorldButtonClicked); 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, getLocalViewingDistance()); mGlobalMap->setVisible(global); mLocalMap->setVisible(!global); } void MapWindow::onNoteEditOk() { if (mEditNoteDialog.getDeleteButtonShown()) mCustomMarkers.updateMarker(mEditingMarker, mEditNoteDialog.getText()); else { mEditingMarker.mNote = mEditNoteDialog.getText(); mCustomMarkers.addMarker(mEditingMarker); } mEditNoteDialog.setVisible(false); } void MapWindow::onNoteEditDelete() { ConfirmationDialog* confirmation = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); confirmation->askForConfirmation("#{sDeleteNote}"); confirmation->eventCancelClicked.clear(); confirmation->eventOkClicked.clear(); confirmation->eventOkClicked += MyGUI::newDelegate(this, &MapWindow::onNoteEditDeleteConfirm); } void MapWindow::onNoteEditDeleteConfirm() { mCustomMarkers.deleteMarker(mEditingMarker); mEditNoteDialog.setVisible(false); } void MapWindow::onCustomMarkerDoubleClicked(MyGUI::Widget* sender) { mEditingMarker = *sender->getUserData(); mEditNoteDialog.setText(mEditingMarker.mNote); mEditNoteDialog.showDeleteButton(true); mEditNoteDialog.setVisible(true); } void MapWindow::onMapDoubleClicked(MyGUI::Widget* sender) { MyGUI::IntPoint clickedPos = MyGUI::InputManager::getInstance().getMousePosition(); MyGUI::IntPoint widgetPos = clickedPos - mEventBoxLocal->getAbsolutePosition(); auto mapWidgetSize = getWidgetSize(); int x = int(widgetPos.left / float(mapWidgetSize)) + mGrid.left; int y = mGrid.bottom - int(widgetPos.top / float(mapWidgetSize)); float nX = widgetPos.left / float(mapWidgetSize) - int(widgetPos.left / float(mapWidgetSize)); float nY = widgetPos.top / float(mapWidgetSize) - int(widgetPos.top / float(mapWidgetSize)); osg::Vec2f worldPos; if (!mActiveCell->isExterior()) { worldPos = mLocalMapRender->interiorMapToWorldPosition(nX, nY, x, y); } else { worldPos.x() = (x + nX) * 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 currentMinLocalMapZoom = std::max({ (float(Settings::map().mGlobalMapCellSize) * 4.f) / float(localWidgetSize), float(mLocalMap->getWidth()) / (localWidgetSize * (mGrid.width() + 1)), float(mLocalMap->getHeight()) / (localWidgetSize * (mGrid.height() + 1)) }); 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())); mGlobalMapZoom *= speedDiff; if (zoomIn && mGlobalMapZoom > 4.f) { mGlobalMapZoom = currentGlobalZoom; mLocalMapZoom = currentMinLocalMapZoom; onWorldButtonClicked(nullptr); updateLocalMap(); return; // the zoom in is too big } if (zoomOut && mGlobalMapZoom < currentMinGlobalMapZoom) { mGlobalMapZoom = currentGlobalZoom; return; // the zoom out is too big, we have reach the borders of the widget } } else { auto const currentLocalZoom = mLocalMapZoom; mLocalMapZoom *= speedDiff; if (zoomIn && mLocalMapZoom > 4.0f) { mLocalMapZoom = currentLocalZoom; return; // the zoom in is too big } if (zoomOut && mLocalMapZoom < currentMinLocalMapZoom) { mLocalMapZoom = currentLocalZoom; float zoomRatio = 4.f / mGlobalMapZoom; mGlobalMapZoom = 4.f; onWorldButtonClicked(nullptr); zoomOnCursor(zoomRatio); return; // the zoom out is too big, we switch to the global map } if (zoomOut) mNeedDoorMarkersUpdate = true; } zoomOnCursor(speedDiff); } void MapWindow::zoomOnCursor(float speedDiff) { auto map = 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 viewPortCenterDiff = currentViewPortCenter - lastViewPortCenter; mLocalMap->setViewOffset(mLocalMap->getViewOffset() + viewPortCenterDiff); mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + viewPortCenterDiff); mLastScrollWindowCoordinates = currentCoordinates; } void MapWindow::setVisible(bool visible) { WindowBase::setVisible(visible); mButton->setVisible(visible && MWBase::Environment::get().getWindowManager()->getMode() != MWGui::GM_None); } void MapWindow::renderGlobalMap() { mGlobalMapRender->render(); resizeGlobalMap(); } 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; cell.first = x; cell.second = y; if (mMarkers.insert(cell).second) { MapMarkerType mapMarkerWidget = { osg::Vec2f(x, y), createMarker(name, x, y, 0) }; mGlobalMapMarkers.emplace(mapMarkerWidget, std::vector()); 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 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); } } } void MapWindow::cellExplored(int x, int y) { mGlobalMapRender->cleanupCameras(); mGlobalMapRender->exploreCell(x, y, mLocalMapRender->getMapTexture(x, y)); } void MapWindow::onFrame(float dt) { LocalMapBase::onFrame(dt); NoDrop::onFrame(dt); } void MapWindow::setGlobalMapMarkerTooltip(MyGUI::Widget* markerWidget, int x, int y) { 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); std::swap(data.notes, destNotes); data.caption = markerWidget->getUserString("Caption_TextOneLine"); markerWidget->setUserData(data); markerWidget->setUserString("ToolTipType", "MapMarker"); } else { markerWidget->setUserString("ToolTipType", "Layout"); } } 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, 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; mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onMouseDrag(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { if (_id != MyGUI::MouseButton::Left) return; MyGUI::IntPoint diff = MyGUI::IntPoint(_left, _top) - mLastDragPos; if (!Settings::map().mGlobal) { mNeedDoorMarkersUpdate = true; mLocalMap->setViewOffset(mLocalMap->getViewOffset() + diff); } else mGlobalMap->setViewOffset(mGlobalMap->getViewOffset() + diff); mLastDragPos = MyGUI::IntPoint(_left, _top); } void MapWindow::onWorldButtonClicked(MyGUI::Widget* _sender) { const bool global = !Settings::map().mGlobal; Settings::map().mGlobal.set(global); mGlobalMap->setVisible(global); mLocalMap->setVisible(!global); mButton->setCaptionWithReplacing(global ? "#{sLocal}" : "#{sWorld}"); } void MapWindow::onPinToggled() { Settings::windows().mMapPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setMinimapVisibility(!mPinned); } void MapWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Map); } void MapWindow::onOpen() { ensureGlobalMapLoaded(); globalMapUpdatePlayer(); } void MapWindow::globalMapUpdatePlayer() { // For interiors, position is set by WindowManager via setGlobalMapPlayerPosition if (MWBase::Environment::get().getWorld()->isCellExterior()) { osg::Vec3f pos = MWBase::Environment::get().getWorld()->getPlayerPtr().getRefData().getPosition().asVec3(); setGlobalMapPlayerPosition(pos.x(), pos.y()); } } void MapWindow::notifyPlayerUpdate() { globalMapUpdatePlayer(); setGlobalMapPlayerDir(mLastDirectionX, mLastDirectionY); } void MapWindow::centerView() { LocalMapBase::centerView(); // set the view offset so that player is in the center MyGUI::IntSize viewsize = mGlobalMap->getSize(); MyGUI::IntPoint pos = mPlayerArrowGlobal->getPosition() + MyGUI::IntPoint{ 16, 16 }; MyGUI::IntPoint viewoffs( static_cast(viewsize.width * 0.5f - pos.left), static_cast(viewsize.height * 0.5f - pos.top)); mGlobalMap->setViewOffset(viewoffs); } void MapWindow::setGlobalMapPlayerPosition(float worldX, float worldY) { float x, y; 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->setAngle(angle); } void MapWindow::ensureGlobalMapLoaded() { if (!mGlobalMapTexture.get()) { mGlobalMapTexture = std::make_unique(mGlobalMapRender->getBaseTexture()); mGlobalMapImage->setRenderItemTexture(mGlobalMapTexture.get()); mGlobalMapImage->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); mGlobalMapOverlayTexture = std::make_unique(mGlobalMapRender->getOverlayTexture()); mGlobalMapOverlay->setRenderItemTexture(mGlobalMapOverlayTexture.get()); mGlobalMapOverlay->getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 0.f, 1.f, 1.f)); // Redraw children in proper order mGlobalMap->getParent()->_updateChilds(); } } void MapWindow::clear() { mMarkers.clear(); mGlobalMapRender->clear(); mActiveCell = nullptr; for (auto& widgetPair : mGlobalMapMarkers) MyGUI::Gui::getInstance().destroyWidget(widgetPair.first.widget); mGlobalMapMarkers.clear(); mGlobalMapMarkersByName.clear(); } void MapWindow::write(ESM::ESMWriter& writer, Loading::Listener& progress) { ESM::GlobalMap map; mGlobalMapRender->write(map); map.mMarkers = mMarkers; writer.startRecord(ESM::REC_GMAP); map.save(writer); writer.endRecord(ESM::REC_GMAP); } void MapWindow::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GMAP) { ESM::GlobalMap map; map.load(reader); mGlobalMapRender->read(map); for (const ESM::GlobalMap::CellId& cellId : map.mMarkers) { 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); } } } void MapWindow::setAlpha(float alpha) { NoDrop::setAlpha(alpha); // can't allow showing map with partial transparency, as the fog of war will also go transparent // and reveal parts of the map you shouldn't be able to see for (MapEntry& entry : mMaps) entry.mMapWidget->setVisible(alpha == 1); } 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) { 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(); } // ------------------------------------------------------------------- EditNoteDialog::EditNoteDialog() : WindowModal("openmw_edit_note.layout") { getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mTextEdit, "TextEdit"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onCancelButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onOkButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditNoteDialog::onDeleteButtonClicked); } void EditNoteDialog::showDeleteButton(bool show) { mDeleteButton->setVisible(show); } bool EditNoteDialog::getDeleteButtonShown() { return mDeleteButton->getVisible(); } void EditNoteDialog::setText(const std::string& text) { mTextEdit->setCaption(MyGUI::TextIterator::toTagsString(text)); } std::string EditNoteDialog::getText() { return MyGUI::TextIterator::getOnlyText(mTextEdit->getCaption()); } void EditNoteDialog::onOpen() { WindowModal::onOpen(); center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } void EditNoteDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); } void EditNoteDialog::onOkButtonClicked(MyGUI::Widget* sender) { eventOkClicked(); } void EditNoteDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { eventDeleteClicked(); } bool LocalMapBase::MarkerUserData::isPositionExplored() const { if (!mLocalMapRender) return true; return mLocalMapRender->isPositionExplored(nX, nY, cellX, cellY); } } openmw-openmw-0.49.0/apps/openmw/mwgui/mapwindow.hpp000066400000000000000000000245601503074453300225450ustar00rootroot00000000000000#ifndef MWGUI_MAPWINDOW_H #define MWGUI_MAPWINDOW_H #include #include #include #include #include "windowpinnablebase.hpp" #include #include namespace MWRender { class GlobalMap; class LocalMap; } namespace ESM { class ESMReader; class ESMWriter; } namespace MWWorld { class Cell; class CellStore; } namespace Loading { class Listener; } namespace SceneUtil { class WorkQueue; } namespace MWGui { class CustomMarkerCollection { public: 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::pair RangeType; ContainerType::const_iterator begin() const; ContainerType::const_iterator end() const; RangeType getMarkers(const ESM::RefId& cellId) const; typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; EventHandle_Void eventMarkersChanged; private: ContainerType mMarkers; }; class LocalMapBase { public: LocalMapBase(CustomMarkerCollection& markers, MWRender::LocalMap* localMapRender, bool fogOfWarEnabled); virtual ~LocalMapBase(); void init(MyGUI::ScrollView* widget, MyGUI::ImageBox* compass, int cellDistance = Constants::CellGridRadius); 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); void onFrame(float dt); bool toggleFogOfWar(); struct MarkerUserData { MarkerUserData(MWRender::LocalMap* map) : mLocalMapRender(map) , cellX(0) , cellY(0) , nX(0.f) , nY(0.f) { } bool isPositionExplored() const; MWRender::LocalMap* mLocalMapRender; int cellX; int cellY; float nX; float nY; std::vector notes; std::string caption; }; protected: void updateLocalMap(); MWRender::LocalMap* mLocalMapRender; const MWWorld::Cell* mActiveCell = nullptr; osg::Vec2f mCurPos; // the position of the player in the world (in cell coords) MyGUI::ScrollView* mLocalMap = nullptr; MyGUI::ImageBox* mCompass = nullptr; float mLocalMapZoom = 1.f; bool mHasALastActiveCell = false; bool mFogOfWarToggled = true; bool mFogOfWarEnabled; bool mNeedDoorMarkersUpdate = false; // Stores markers that were placed by a player. May be shared between multiple map views. CustomMarkerCollection& mCustomMarkers; struct MapEntry { MapEntry(MyGUI::ImageBox* mapWidget, MyGUI::ImageBox* fogWidget) : mMapWidget(mapWidget) , mFogWidget(fogWidget) , mCellX(0) , mCellY(0) { } MyGUI::ImageBox* mMapWidget; MyGUI::ImageBox* mFogWidget; 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 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 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) {} virtual void doorMarkerCreated(MyGUI::Widget* marker) {} void updateRequiredMaps(); void updateMagicMarkers(); void addDetectionMarkers(int type); void redraw(); float getWidgetSize() const; MWGui::LocalMapBase::MapEntry& addMapEntry(); MyGUI::IntRect mGrid{ -1, -1, 1, 1 }; int mExtCellDistance = 0; float mMarkerUpdateTimer = 0.f; float mLastDirectionX = 0.f; float mLastDirectionY = 0.f; private: void updateDoorMarkers(); }; class EditNoteDialog : public MWGui::WindowModal { public: EditNoteDialog(); void onOpen() override; void showDeleteButton(bool show); bool getDeleteButtonShown(); void setText(const std::string& text); std::string getText(); typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; EventHandle_Void eventDeleteClicked; EventHandle_Void eventOkClicked; private: void onCancelButtonClicked(MyGUI::Widget* sender); void onOkButtonClicked(MyGUI::Widget* sender); void onDeleteButtonClicked(MyGUI::Widget* sender); MyGUI::TextBox* mTextEdit; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; MyGUI::Button* mDeleteButton; }; class MapWindow : public MWGui::WindowPinnableBase, public LocalMapBase, public NoDrop { public: MapWindow(CustomMarkerCollection& customMarkers, DragAndDrop* drag, MWRender::LocalMap* localMapRender, SceneUtil::WorkQueue* workQueue); virtual ~MapWindow(); void setCellName(const std::string& cellName); void setAlpha(float alpha) override; void setVisible(bool visible) override; void renderGlobalMap(); /// adds the marker to the global map /// @param name The ESM::Cell::mName void addVisitedLocation(const std::string& name, int x, int y); // reveals this cell's map on the global map void cellExplored(int x, int y); void setGlobalMapPlayerPosition(float worldX, float worldY); void setGlobalMapPlayerDir(const float x, const float y); void ensureGlobalMapLoaded(); void onOpen() override; void onFrame(float dt) override; void updateCustomMarkers() override; /// 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 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(); void onNoteEditDeleteConfirm(); void onNoteDoubleClicked(MyGUI::Widget* sender); 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; std::unique_ptr mGlobalMapOverlayTexture; MyGUI::ImageBox* mGlobalMapImage; MyGUI::ImageBox* mGlobalMapOverlay; MyGUI::ImageBox* mPlayerArrowLocal; MyGUI::ImageBox* mPlayerArrowGlobal; MyGUI::Button* mButton; MyGUI::IntPoint mLastDragPos; MyGUI::IntCoord mLastScrollWindowCoordinates; // Markers on global map typedef std::pair CellId; std::set mMarkers; MyGUI::Button* mEventBoxGlobal; MyGUI::Button* mEventBoxLocal; 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 mGlobalMapMarkersByName; std::map> mGlobalMapMarkers; EditNoteDialog mEditNoteDialog; ESM::CustomMarker mEditingMarker; void onPinToggled() override; void onTitleDoubleClicked() override; void doorMarkerCreated(MyGUI::Widget* marker) override; void customMarkerCreated(MyGUI::Widget* marker) override; void notifyPlayerUpdate() override; void centerView() override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/merchantrepair.cpp000066400000000000000000000145311503074453300235340ustar00rootroot00000000000000#include "merchantrepair.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" namespace MWGui { MerchantRepair::MerchantRepair() : WindowBase("openmw_merchantrepair.layout") { getWidget(mList, "RepairView"); getWidget(mOkButton, "OkButton"); getWidget(mGoldLabel, "PlayerGold"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MerchantRepair::onOkButtonClick); } 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)); 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::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) { 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(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)); } 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::onOpen() { center(); // Reset scrollbars mList->setViewOffset(MyGUI::IntPoint(0, 0)); } 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; // repair MWWorld::Ptr item = *sender->getUserData(); item.getCellRef().setCharge(item.getClass().getItemMaxHealth(item)); player.getClass().getContainerStore(player).restack(item); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Repair")); 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); setPtr(mActor); } void MerchantRepair::onOkButtonClick(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_MerchantRepair); } } openmw-openmw-0.49.0/apps/openmw/mwgui/merchantrepair.hpp000066400000000000000000000014101503074453300235310ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_MERCHANTREPAIR_H #define OPENMW_MWGUI_MERCHANTREPAIR_H #include "../mwworld/ptr.hpp" #include "windowbase.hpp" namespace MWGui { class MerchantRepair : public WindowBase { public: MerchantRepair(); void onOpen() 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; MWWorld::Ptr mActor; protected: void onMouseWheel(MyGUI::Widget* _sender, int _rel); void onRepairButtonClick(MyGUI::Widget* sender); void onOkButtonClick(MyGUI::Widget* sender); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/messagebox.cpp000066400000000000000000000337461503074453300226760ustar00rootroot00000000000000#include "messagebox.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" namespace MWGui { MessageBoxManager::MessageBoxManager(float timePerChar) { mStaticMessageBox = nullptr; mLastButtonPressed = -1; mMessageBoxSpeed = timePerChar; } MessageBoxManager::~MessageBoxManager() { MessageBoxManager::clear(); } std::size_t MessageBoxManager::getMessagesCount() { return mMessageBoxes.size(); } void MessageBoxManager::clear() { if (mInterMessageBoxe) { mInterMessageBoxe->setVisible(false); mInterMessageBoxe.reset(); } mMessageBoxes.clear(); mStaticMessageBox = nullptr; mLastButtonPressed = -1; } void MessageBoxManager::resetInteractiveMessageBox() { if (mInterMessageBoxe) { mInterMessageBoxe->setVisible(false); mInterMessageBoxe.reset(); } } void MessageBoxManager::setLastButtonPressed(int index) { mLastButtonPressed = index; } void MessageBoxManager::onFrame(float frameDuration) { for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) { (*it)->mCurrentTime += frameDuration; if ((*it)->mCurrentTime >= (*it)->mMaxTime && it->get() != mStaticMessageBox) { it = mMessageBoxes.erase(it); } else ++it; } float height = 0; auto it = mMessageBoxes.begin(); while (it != mMessageBoxes.end()) { (*it)->update(static_cast(height)); height += (*it)->getHeight(); ++it; } if (mInterMessageBoxe != nullptr && mInterMessageBoxe->mMarkedToDelete) { mLastButtonPressed = mInterMessageBoxe->readPressedButton(); mInterMessageBoxe->setVisible(false); mInterMessageBoxe.reset(); MWBase::Environment::get().getInputManager()->changeInputMode( MWBase::Environment::get().getWindowManager()->isGuiMode()); } } void MessageBoxManager::createMessageBox(std::string_view message, bool stat) { auto box = std::make_unique(*this, message); box->mCurrentTime = 0; auto realMessage = MyGUI::LanguageManager::getInstance().replaceTags({ message.data(), message.size() }); box->mMaxTime = realMessage.length() * mMessageBoxSpeed; if (stat) mStaticMessageBox = box.get(); box->setVisible(mVisible); mMessageBoxes.push_back(std::move(box)); if (mMessageBoxes.size() > 3) { mMessageBoxes.erase(mMessageBoxes.begin()); } int height = 0; for (const auto& messageBox : mMessageBoxes) { messageBox->update(height); height += messageBox->getHeight(); } } void MessageBoxManager::removeStaticMessageBox() { removeMessageBox(mStaticMessageBox); mStaticMessageBox = nullptr; } 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); } mInterMessageBoxe = std::make_unique(*this, std::string{ message }, buttons, immediate, defaultFocus); mLastButtonPressed = -1; return true; } bool MessageBoxManager::isInteractiveMessageBox() { return mInterMessageBoxe != nullptr; } bool MessageBoxManager::removeMessageBox(MessageBox* msgbox) { for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end(); ++it) { if (it->get() == msgbox) { mMessageBoxes.erase(it); return true; } } return false; } const std::vector>& MessageBoxManager::getActiveMessageBoxes() const { return mMessageBoxes; } int MessageBoxManager::readPressedButton(bool reset) { int pressed = mLastButtonPressed; if (reset) mLastButtonPressed = -1; return pressed; } void MessageBoxManager::setVisible(bool value) { mVisible = value; for (const auto& messageBox : mMessageBoxes) messageBox->setVisible(value); } MessageBox::MessageBox(MessageBoxManager& parMessageBoxManager, std::string_view message) : Layout("openmw_messagebox.layout") , mCurrentTime(0) , mMaxTime(0) , mMessageBoxManager(parMessageBoxManager) , mMessage(message) { // defines mBottomPadding = 48; mNextBoxPadding = 4; getWidget(mMessageWidget, "message"); mMessageWidget->setCaptionWithReplacing(mMessage); } void MessageBox::update(int height) { MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); MyGUI::IntPoint pos; pos.left = (gameWindowSize.width - mMainWidget->getWidth()) / 2; pos.top = (gameWindowSize.height - mMainWidget->getHeight() - height - mBottomPadding); mMainWidget->setPosition(pos); } int MessageBox::getHeight() { return mMainWidget->getHeight() + mNextBoxPadding; } void MessageBox::setVisible(bool value) { mMainWidget->setVisible(value); } 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 int buttonLeftPadding = 10; // padding between the buttons if horizontal int buttonTopPadding = 10; // ^-- if vertical int buttonLabelLeftPadding = 12; // padding between button label and button itself, from left int buttonLabelTopPadding = 4; // padding between button label and button itself, from top int buttonMainPadding = 10; // padding between buttons and bottom of the main widget mMarkedToDelete = false; getWidget(mMessageWidget, "message"); getWidget(mButtonsWidget, "buttons"); mMessageWidget->setSize(400, mMessageWidget->getHeight()); mMessageWidget->setCaptionWithReplacing(message); MyGUI::IntSize textSize = mMessageWidget->getTextSize(); MyGUI::IntSize gameWindowSize = MyGUI::RenderManager::getInstance().getViewSize(); int biggestButtonWidth = 0; int buttonsWidth = 0; int buttonsHeight = 0; int buttonHeight = 0; MyGUI::IntCoord dummyCoord(0, 0, 0, 0); for (const std::string& buttonId : buttons) { MyGUI::Button* button = mButtonsWidget->createWidget( MyGUI::WidgetStyle::Child, std::string("MW_Button"), dummyCoord, MyGUI::Align::Default); button->setCaptionWithReplacing(buttonId); button->eventMouseButtonClick += MyGUI::newDelegate(this, &InteractiveMessageBox::mousePressed); mButtons.push_back(button); if (buttonsWidth != 0) buttonsWidth += buttonLeftPadding; int buttonWidth = button->getTextSize().width + 2 * buttonLabelLeftPadding; buttonsWidth += buttonWidth; buttonHeight = button->getTextSize().height + 2 * buttonLabelTopPadding; if (buttonsHeight != 0) buttonsHeight += buttonTopPadding; buttonsHeight += buttonHeight; if (buttonWidth > biggestButtonWidth) { biggestButtonWidth = buttonWidth; } } MyGUI::IntSize mainWidgetSize; if (buttonsWidth < textSize.width) { // on one line 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()); MyGUI::IntPoint absPos; 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.top = textPadding; mMessageWidget->setCoord(messageWidgetCoord); mMessageWidget->setSize(textSize); MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); int left = (mainWidgetSize.width - buttonsWidth) / 2; 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; button->setCoord(buttonCord); button->setSize(buttonSize); left += buttonSize.width + buttonLeftPadding; } } else { // among each other if (biggestButtonWidth > textSize.width) { mainWidgetSize.width = biggestButtonWidth + buttonTopPadding * 2; } else { mainWidgetSize.width = textSize.width + 3 * textPadding; } MyGUI::IntCoord buttonCord; MyGUI::IntSize buttonSize(0, buttonHeight); int top = textPadding + textSize.height + textButtonPadding; for (MyGUI::Button* button : mButtons) { buttonSize.width = button->getTextSize().width + buttonLabelLeftPadding * 2; buttonSize.height = button->getTextSize().height + buttonLabelTopPadding * 2; buttonCord.top = top; buttonCord.left = (mainWidgetSize.width - buttonSize.width) / 2; button->setCoord(buttonCord); button->setSize(buttonSize); top += buttonSize.height + buttonTopPadding; } mainWidgetSize.height = textPadding + textSize.height + textButtonPadding + buttonsHeight + buttonMainPadding; mMainWidget->setSize(mainWidgetSize + // 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; mMainWidget->setPosition(absPos); MyGUI::IntCoord messageWidgetCoord; messageWidgetCoord.left = (mainWidgetSize.width - textSize.width) / 2; messageWidgetCoord.top = textPadding; messageWidgetCoord.width = textSize.width; messageWidgetCoord.height = textSize.height; mMessageWidget->setCoord(messageWidgetCoord); } setVisible(true); } MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { 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 MyGUI::UString& keyword : keywords) { if (Misc::StringUtils::ciEqual(keyword, button->getCaption())) { return button; } } } return nullptr; } void InteractiveMessageBox::mousePressed(MyGUI::Widget* pressed) { buttonActivated(pressed); } void InteractiveMessageBox::buttonActivated(MyGUI::Widget* pressed) { mMarkedToDelete = true; int index = 0; for (const MyGUI::Button* button : mButtons) { 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() { return mButtonPressed; } } openmw-openmw-0.49.0/apps/openmw/mwgui/messagebox.hpp000066400000000000000000000064441503074453300226760ustar00rootroot00000000000000#ifndef MWGUI_MESSAGE_BOX_H #define MWGUI_MESSAGE_BOX_H #include #include "windowbase.hpp" namespace MyGUI { class Widget; class Button; class EditBox; } namespace MWGui { class InteractiveMessageBox; class MessageBoxManager; class MessageBox; class MessageBoxManager { 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); /// @param reset Reset the pressed button to -1 after reading it. int readPressedButton(bool reset = true); void resetInteractiveMessageBox(); void setLastButtonPressed(int index); typedef MyGUI::delegates::MultiDelegate EventHandle_Int; // Note: this delegate unassigns itself after it was fired, i.e. works once. EventHandle_Int eventButtonPressed; void onButtonPressed(int button) { eventButtonPressed(button); eventButtonPressed.clear(); } void setVisible(bool value); const std::vector>& getActiveMessageBoxes() const; 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, 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, bool immediate, int defaultFocus); void mousePressed(MyGUI::Widget* _widget); int readPressedButton(); MyGUI::Widget* getDefaultKeyFocus() override; bool exit() override { return false; } bool mMarkedToDelete; private: void buttonActivated(MyGUI::Widget* _widget); MessageBoxManager& mMessageBoxManager; MyGUI::EditBox* mMessageWidget; MyGUI::Widget* mButtonsWidget; std::vector mButtons; int mButtonPressed; int mDefaultFocus; bool mImmediate; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/mode.hpp000066400000000000000000000022311503074453300214530ustar00rootroot00000000000000#ifndef MWGUI_MODE_H #define MWGUI_MODE_H namespace MWGui { enum GuiMode { GM_None, GM_Inventory, // Inventory mode GM_Container, GM_Companion, GM_MainMenu, // Main menu mode GM_Journal, // Journal mode 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_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, GM_QuickKeysMenu }; // Windows shown in inventory mode enum GuiWindow { GW_None = 0, GW_Map = 0x01, GW_Inventory = 0x02, GW_Magic = 0x04, GW_Stats = 0x08, GW_ALL = 0xFF }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/pickpocketitemmodel.cpp000066400000000000000000000113401503074453300245570ustar00rootroot00000000000000#include "pickpocketitemmodel.hpp" #include #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/pickpocket.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" namespace MWGui { PickpocketItemModel::PickpocketItemModel( const MWWorld::Ptr& actor, std::unique_ptr sourceModel, bool hideItems) : mActor(actor) , mPickpocketDetected(false) { MWWorld::Ptr player = MWMechanics::getPlayer(); 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) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { if (Misc::Rng::roll0to99(prng) > chance) mHiddenItems.push_back(mSourceModel->getItem(i)); } } } bool PickpocketItemModel::allowedToUseItems() const { return false; } ItemStack PickpocketItemModel::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 PickpocketItemModel::getItemCount() { return mItems.size(); } void PickpocketItemModel::update() { mSourceModel->update(); mItems.clear(); for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { const ItemStack& item = mSourceModel->getItem(i); // Bound items may not be stolen if (item.mFlags & ItemStack::Flag_Bound) continue; if (std::find(mHiddenItems.begin(), mHiddenItems.end(), item) == mHiddenItems.end() && item.mType != ItemStack::Type_Equipped) mItems.push_back(item); } } void PickpocketItemModel::removeItem(const ItemStack& item, size_t count) { ProxyItemModel::removeItem(item, 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; } void PickpocketItemModel::onClose() { // 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) return; MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::Pickpocket pickpocket(player, mActor); if (pickpocket.finish()) { MWBase::Environment::get().getMechanicsManager()->commitCrime( player, mActor, MWBase::MechanicsManager::OT_Pickpocket, ESM::RefId(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; } } bool PickpocketItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { if (mActor.getClass().getCreatureStats(mActor).getKnockedDown()) return mSourceModel->onTakeItem(item, count); bool success = stealItem(item, count); if (success) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWBase::Environment::get().getMechanicsManager()->itemTaken(player, item, mActor, count, false); } return success; } 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, ESM::RefId(), 0, true); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Container); mPickpocketDetected = true; return false; } else player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_PickPocket); return true; } } openmw-openmw-0.49.0/apps/openmw/mwgui/pickpocketitemmodel.hpp000066400000000000000000000021521503074453300245650ustar00rootroot00000000000000#ifndef MWGUI_PICKPOCKET_ITEM_MODEL_H #define MWGUI_PICKPOCKET_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { /// @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, std::unique_ptr sourceModel, bool hideItems = true); bool allowedToUseItems() const override; ItemStack getItem(ModelIndex index) override; size_t getItemCount() override; void update() 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; protected: MWWorld::Ptr mActor; bool mPickpocketDetected; bool stealItem(const MWWorld::Ptr& item, int count); private: std::vector mHiddenItems; std::vector mItems; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/postprocessorhud.cpp000066400000000000000000000461361503074453300241640ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/postprocessorhud.hpp000066400000000000000000000044551503074453300241670ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwgui/quickkeysmenu.cpp000066400000000000000000000540271503074453300234310ustar00rootroot00000000000000#include "quickkeysmenu.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellutil.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" #include "spellview.hpp" namespace MWGui { QuickKeysMenu::QuickKeysMenu() : WindowBase("openmw_quickkeys_menu.layout") , mKey(std::vector(10)) , mSelected(nullptr) , mActivated(nullptr) { getWidget(mOkButton, "OKButton"); getWidget(mInstructionLabel, "InstructionLabel"); mMainWidget->setSize(mMainWidget->getWidth(), 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].button->eventMouseButtonClick += MyGUI::newDelegate(this, &QuickKeysMenu::onQuickKeyButtonClicked); unassign(&mKey[i]); } } void QuickKeysMenu::clear() { mActivated = nullptr; for (int i = 0; i < 10; ++i) { unassign(&mKey[i]); } } inline void QuickKeysMenu::validate(int index) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); switch (mKey[index].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: { 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)) { // Try searching for a compatible replacement item = store.findReplacement(mKey[index].id); if (!item.isEmpty()) mKey[index].button->setUserData(MWWorld::Ptr(item)); 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(); key->button->setItem(MWWorld::Ptr()); while (key->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(key->button->getChildAt(0)); if (key->index == 10) { key->type = ESM::QuickKeys::Type::HandToHand; 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 = 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); textBox->setTextAlign(MyGUI::Align::Center); textBox->setCaption(MyGUI::utility::toString(key->index)); textBox->setNeedMouseFocus(false); } } void QuickKeysMenu::onQuickKeyButtonClicked(MyGUI::Widget* sender) { int index = -1; for (int i = 0; i < 10; ++i) { if (sender == mKey[i].button || sender->getParent() == mKey[i].button) { index = i; break; } } assert(index != -1); if (index < 0) { mSelected = nullptr; return; } mSelected = &mKey[index]; // prevent reallocation of zero key from ESM::QuickKeys::Type::HandToHand if (mSelected->index == 10) return; // open assign dialog if (!mAssignDialog) mAssignDialog = std::make_unique(this); mAssignDialog->setVisible(true); } void QuickKeysMenu::onOkButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_QuickKeysMenu); } void QuickKeysMenu::onItemButtonClicked(MyGUI::Widget* sender) { if (!mItemSelectionDialog) { mItemSelectionDialog = std::make_unique("#{sQuickMenu6}"); mItemSelectionDialog->eventItemSelected += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItem); mItemSelectionDialog->eventDialogCanceled += MyGUI::newDelegate(this, &QuickKeysMenu::onAssignItemCancel); } mItemSelectionDialog->setVisible(true); mItemSelectionDialog->openContainer(MWMechanics::getPlayer()); mItemSelectionDialog->setFilter(SortFilterItemModel::Filter_OnlyUsableItems); mAssignDialog->setVisible(false); } void QuickKeysMenu::onMagicButtonClicked(MyGUI::Widget* sender) { if (!mMagicSelectionDialog) { mMagicSelectionDialog = std::make_unique(this); } mMagicSelectionDialog->setVisible(true); mAssignDialog->setVisible(false); } void QuickKeysMenu::onUnassignButtonClicked(MyGUI::Widget* sender) { unassign(mSelected); mAssignDialog->setVisible(false); } void QuickKeysMenu::onCancelButtonClicked(MyGUI::Widget* sender) { mAssignDialog->setVisible(false); } void QuickKeysMenu::onAssignItem(MWWorld::Ptr item) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); mSelected->type = ESM::QuickKeys::Type::Item; mSelected->id = item.getCellRef().getRefId(); mSelected->name = item.getClass().getName(item); mSelected->button->setItem(item, ItemWidget::Barter); mSelected->button->setUserString("ToolTipType", "ItemPtr"); mSelected->button->setUserData(item); if (mItemSelectionDialog) mItemSelectionDialog->setVisible(false); } void QuickKeysMenu::onAssignItemCancel() { mItemSelectionDialog->setVisible(false); } void QuickKeysMenu::onAssignMagicItem(MWWorld::Ptr item) { assert(mSelected); while (mSelected->button->getChildCount()) // Destroy number label MyGUI::Gui::getInstance().destroyWidget(mSelected->button->getChildAt(0)); 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"); 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->setIcon(item); mSelected->button->setUserString("ToolTipType", "ItemPtr"); mSelected->button->setUserData(MWWorld::Ptr(item)); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); } 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().getESMStore(); const ESM::Spell* spell = esmStore.get().find(spellId); 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.serialize()); // use the icon of the first effect 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 = 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"); 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->setIcon(path); if (mMagicSelectionDialog) mMagicSelectionDialog->setVisible(false); } void QuickKeysMenu::onAssignMagicCancel() { mMagicSelectionDialog->setVisible(false); } void QuickKeysMenu::updateActivatedQuickKey() { // there is no delayed action, nothing to do. if (!mActivated) return; activateQuickKey(mActivated->index); } void QuickKeysMenu::activateQuickKey(int index) { assert(index >= 1 && index <= 10); keyData* key = &mKey[index - 1]; MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); const MWMechanics::CreatureStats& playerStats = player.getClass().getCreatureStats(player); validate(index - 1); // Delay action executing, // if player is busy for now (casting a spell, attacking someone, etc.) bool isDelayNeeded = MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player) || playerStats.getKnockedDown() || playerStats.getHitRecovery(); bool isReturnNeeded = playerStats.isParalyzed() || playerStats.isDead(); if (isReturnNeeded) { return; } else if (isDelayNeeded) { mActivated = key; return; } else { mActivated = nullptr; } if (key->type == ESM::QuickKeys::Type::Item || key->type == ESM::QuickKeys::Type::MagicItem) { MWWorld::Ptr item = *key->button->getUserData(); MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) { if (*it == item) break; } if (it == store.end()) item = nullptr; // check the item is available and not broken if (item.isEmpty() || item.getCellRef().getCount() < 1 || (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) <= 0)) { item = store.findReplacement(key->id); if (item.isEmpty() || item.getCellRef().getCount() < 1) { MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); return; } } if (key->type == ESM::QuickKeys::Type::Item) { if (!store.isEquipped(item)) MWBase::Environment::get().getWindowManager()->useItem(item); 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); } } 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)) { MWBase::Environment::get().getWindowManager()->useItem(item); // make sure that item was successfully equipped if (!store.isEquipped(item)) return; } store.setSelectedEnchantItem(it); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } } else if (key->type == ESM::QuickKeys::Type::Magic) { const ESM::RefId& spellId = key->id; // Make sure the player still has this spell MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); if (!spells.hasSpell(spellId)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sQuickMenu5} " + key->name); return; } store.setSelectedEnchantItem(store.end()); MWBase::Environment::get().getWindowManager()->setSelectedSpell( spellId, int(MWMechanics::getSpellSuccessChance(spellId, player))); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Spell); } else if (key->type == ESM::QuickKeys::Type::HandToHand) { store.unequipSlot(MWWorld::InventoryStore::Slot_CarriedRight); MWBase::Environment::get().getWorld()->getPlayer().setDrawState(MWMechanics::DrawState::Weapon); } } // --------------------------------------------------------------------------------------------------------- QuickKeysMenuAssign::QuickKeysMenuAssign(QuickKeysMenu* parent) : WindowModal("openmw_quickkeys_menu_assign.layout") , mParent(parent) { getWidget(mLabel, "Label"); getWidget(mItemButton, "ItemButton"); getWidget(mMagicButton, "MagicButton"); getWidget(mUnassignButton, "UnassignButton"); getWidget(mCancelButton, "CancelButton"); mItemButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onItemButtonClicked); mMagicButton->eventMouseButtonClick += MyGUI::newDelegate(mParent, &QuickKeysMenu::onMagicButtonClicked); 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); 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()); center(); } 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) { ItemWidget* button = mKey[i].button; const ESM::QuickKeys::Type type = mKey[i].type; ESM::QuickKeys::QuickKey key; key.mType = type; switch (type) { case ESM::QuickKeys::Type::Unassigned: case ESM::QuickKeys::Type::HandToHand: break; case ESM::QuickKeys::Type::Item: case ESM::QuickKeys::Type::MagicItem: { MWWorld::Ptr item = *button->getUserData(); key.mId = item.getCellRef().getRefId(); break; } case ESM::QuickKeys::Type::Magic: key.mId = ESM::RefId::deserialize(button->getUserString("Spell")); break; } keys.mKeys.push_back(key); } keys.save(writer); writer.endRecord(ESM::REC_KEYS); } void QuickKeysMenu::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type != ESM::REC_KEYS) return; ESM::QuickKeys keys; keys.load(reader); MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); 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 if (i >= 9) return; mSelected = &mKey[i]; switch (quickKey.mType) { 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: { // Find the item by id MWWorld::Ptr item = store.findReplacement(quickKey.mId); 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; } } // --------------------------------------------------------------------------------------------------------- MagicSelectionDialog::MagicSelectionDialog(QuickKeysMenu* parent) : WindowModal("openmw_magicselection_dialog.layout") , mParent(parent) { getWidget(mCancelButton, "CancelButton"); getWidget(mMagicList, "MagicList"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &MagicSelectionDialog::onCancelButtonClicked); mMagicList->setShowCostColumn(false); mMagicList->setHighlightSelected(false); mMagicList->eventSpellClicked += MyGUI::newDelegate(this, &MagicSelectionDialog::onModelIndexSelected); center(); } void MagicSelectionDialog::onCancelButtonClicked(MyGUI::Widget* sender) { exit(); } bool MagicSelectionDialog::exit() { mParent->onAssignMagicCancel(); return true; } void MagicSelectionDialog::onOpen() { WindowModal::onOpen(); mMagicList->setModel(new SpellModel(MWMechanics::getPlayer())); mMagicList->resetScrollbars(); } void MagicSelectionDialog::onModelIndexSelected(SpellModel::ModelIndex index) { const Spell& spell = mMagicList->getModel()->getItem(index); if (spell.mType == Spell::Type_EnchantedItem) mParent->onAssignMagicItem(spell.mItem); else mParent->onAssignMagic(spell.mId); } } openmw-openmw-0.49.0/apps/openmw/mwgui/quickkeysmenu.hpp000066400000000000000000000056431503074453300234360ustar00rootroot00000000000000#ifndef MWGUI_QUICKKEYS_H #define MWGUI_QUICKKEYS_H #include #include "components/esm3/quickkeys.hpp" #include "itemselection.hpp" #include "spellmodel.hpp" #include "windowbase.hpp" namespace MWGui { class QuickKeysMenuAssign; class MagicSelectionDialog; class ItemWidget; class SpellView; class QuickKeysMenu : public WindowBase { public: QuickKeysMenu(); void onResChange(int, int) override { center(); } void onItemButtonClicked(MyGUI::Widget* sender); void onMagicButtonClicked(MyGUI::Widget* sender); 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 ESM::RefId& spellId); void onAssignMagicCancel(); void onOpen() override; void onClose() override; void activateQuickKey(int index); void updateActivatedQuickKey(); 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 = -1; ItemWidget* button = nullptr; ESM::QuickKeys::Type type = ESM::QuickKeys::Type::Unassigned; ESM::RefId id; std::string name; }; std::vector mKey; keyData* mSelected; keyData* mActivated; MyGUI::EditBox* mInstructionLabel; MyGUI::Button* mOkButton; 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); }; class QuickKeysMenuAssign : public WindowModal { public: QuickKeysMenuAssign(QuickKeysMenu* parent); private: MyGUI::TextBox* mLabel; MyGUI::Button* mItemButton; MyGUI::Button* mMagicButton; MyGUI::Button* mUnassignButton; MyGUI::Button* mCancelButton; QuickKeysMenu* mParent; }; class MagicSelectionDialog : public WindowModal { public: MagicSelectionDialog(QuickKeysMenu* parent); void onOpen() override; bool exit() override; private: MyGUI::Button* mCancelButton; SpellView* mMagicList; QuickKeysMenu* mParent; void onCancelButtonClicked(MyGUI::Widget* sender); void onModelIndexSelected(SpellModel::ModelIndex index); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/race.cpp000066400000000000000000000362141503074453300214440ustar00rootroot00000000000000#include "race.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwrender/characterpreview.hpp" #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" namespace { int wrap(int index, int max) { if (index < 0) return max - 1; else if (index >= max) return 0; else return index; } bool sortRaces(const std::pair& left, const std::pair& right) { return left.second.compare(right.second) < 0; } } 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) { // Centre dialog center(); 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); mHeadRotate->setScrollPosition(500); mHeadRotate->setScrollViewPage(50); mHeadRotate->setScrollPage(50); mHeadRotate->setScrollWheelPage(50); mHeadRotate->eventScrollChangePosition += MyGUI::newDelegate(this, &RaceDialog::onHeadRotate); // Set up next/previous buttons MyGUI::Button *prevButton, *nextButton; 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")); 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")); getWidget(prevButton, "PrevHairButton"); getWidget(nextButton, "NextHairButton"); prevButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectPreviousHair); nextButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onSelectNextHair); setText("RaceT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu5", "Race")); getWidget(mRaceList, "RaceList"); mRaceList->setScrollVisible(true); mRaceList->eventListSelectAccept += MyGUI::newDelegate(this, &RaceDialog::onAccept); mRaceList->eventListChangePosition += MyGUI::newDelegate(this, &RaceDialog::onSelectRace); setText("SkillsT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sBonusSkillTitle", "Skill Bonus")); getWidget(mSkillList, "SkillList"); setText("SpellPowerT", MWBase::Environment::get().getWindowManager()->getGameSettingString("sRaceMenu7", "Specials")); getWidget(mSpellPowerList, "SpellPowerList"); MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &RaceDialog::onOkClicked); updateRaces(); updateSkills(); updateSpellPowers(); } void RaceDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void RaceDialog::onOpen() { WindowModal::onOpen(); updateRaces(); updateSkills(); updateSpellPowers(); mPreviewImage->setRenderItemTexture(nullptr); mPreview.reset(nullptr); mPreviewTexture.reset(nullptr); mPreview = std::make_unique(mParent, mResourceSystem); mPreview->rebuild(); mPreview->setAngle(mCurrentAngle); 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)); const ESM::NPC& proto = mPreview->getPrototype(); setRaceId(proto.mRace); setGender(proto.isMale() ? GM_Male : GM_Female); recountParts(); for (size_t i = 0; i < mAvailableHeads.size(); ++i) { if (mAvailableHeads[i] == proto.mHead) mFaceIndex = i; } for (size_t i = 0; i < mAvailableHairs.size(); ++i) { if (mAvailableHairs[i] == proto.mHair) mHairIndex = i; } mPreviewDirty = true; 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 ESM::RefId& raceId) { mCurrentRaceId = raceId; mRaceList->setIndexSelected(MyGUI::ITEM_NONE); size_t count = mRaceList->getItemCount(); for (size_t i = 0; i < count; ++i) { if (*mRaceList->getItemDataAt(i) == raceId) { mRaceList->setIndexSelected(i); break; } } updateSkills(); updateSpellPowers(); } void RaceDialog::onClose() { WindowModal::onClose(); mPreviewImage->setRenderItemTexture(nullptr); mPreviewTexture.reset(nullptr); mPreview.reset(nullptr); } // widget controls void RaceDialog::onOkClicked(MyGUI::Widget* _sender) { if (mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void RaceDialog::onBackClicked(MyGUI::Widget* _sender) { 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); mCurrentAngle = angle; } void RaceDialog::onSelectPreviousGender(MyGUI::Widget*) { mGenderIndex = wrap(mGenderIndex - 1, 2); recountParts(); updatePreview(); } void RaceDialog::onSelectNextGender(MyGUI::Widget*) { mGenderIndex = wrap(mGenderIndex + 1, 2); recountParts(); updatePreview(); } void RaceDialog::onSelectPreviousFace(MyGUI::Widget*) { mFaceIndex = wrap(mFaceIndex - 1, mAvailableHeads.size()); updatePreview(); } void RaceDialog::onSelectNextFace(MyGUI::Widget*) { mFaceIndex = wrap(mFaceIndex + 1, mAvailableHeads.size()); updatePreview(); } void RaceDialog::onSelectPreviousHair(MyGUI::Widget*) { mHairIndex = wrap(mHairIndex - 1, mAvailableHairs.size()); updatePreview(); } void RaceDialog::onSelectNextHair(MyGUI::Widget*) { mHairIndex = wrap(mHairIndex + 1, mAvailableHairs.size()); updatePreview(); } void RaceDialog::onSelectRace(MyGUI::ListBox* _sender, size_t _index) { if (_index == MyGUI::ITEM_NONE) return; ESM::RefId& raceId = *mRaceList->getItemDataAt(_index); if (mCurrentRaceId == raceId) return; mCurrentRaceId = raceId; recountParts(); updatePreview(); updateSkills(); updateSpellPowers(); } void RaceDialog::onAccept(MyGUI::ListBox* _sender, size_t _index) { onSelectRace(_sender, _index); if (mRaceList->getIndexSelected() == MyGUI::ITEM_NONE) return; eventDone(this); } void RaceDialog::getBodyParts(int part, std::vector& out) { out.clear(); const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); for (const ESM::BodyPart& bodypart : store) { if (bodypart.mData.mFlags & ESM::BodyPart::BPF_NotPlayable) continue; if (bodypart.mData.mType != ESM::BodyPart::MT_Skin) continue; if (bodypart.mData.mPart != static_cast(part)) continue; if (mGenderIndex != (bodypart.mData.mFlags & ESM::BodyPart::BPF_Female)) continue; if (ESM::isFirstPersonBodyPart(bodypart)) continue; if (bodypart.mRace == mCurrentRaceId) out.push_back(bodypart.mId); } } void RaceDialog::recountParts() { getBodyParts(ESM::BodyPart::MP_Hair, mAvailableHairs); getBodyParts(ESM::BodyPart::MP_Head, mAvailableHeads); mFaceIndex = 0; mHairIndex = 0; } // update widget content void RaceDialog::updatePreview() { ESM::NPC record = mPreview->getPrototype(); record.mRace = mCurrentRaceId; record.setIsMale(mGenderIndex == 0); if (mFaceIndex >= 0 && mFaceIndex < int(mAvailableHeads.size())) record.mHead = mAvailableHeads[mFaceIndex]; if (mHairIndex >= 0 && mHairIndex < int(mAvailableHairs.size())) record.mHair = mAvailableHairs[mHairIndex]; try { mPreview->setPrototype(record); } catch (std::exception& e) { Log(Debug::Error) << "Error creating preview: " << e.what(); } } void RaceDialog::updateRaces() { mRaceList->removeAllItems(); const MWWorld::Store& races = MWBase::Environment::get().getESMStore()->get(); std::vector> items; // ID, name for (const ESM::Race& race : races) { bool playable = race.mData.mFlags & ESM::Race::Playable; if (!playable) // Only display playable races continue; items.emplace_back(race.mId, race.mName); } std::sort(items.begin(), items.end(), sortRaces); int index = 0; for (auto& item : items) { mRaceList->addItem(item.second, item.first); if (item.first == mCurrentRaceId) mRaceList->setIndexSelected(index); ++index; } } void RaceDialog::updateSkills() { for (MyGUI::Widget* widget : mSkillItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSkillItems.clear(); if (mCurrentRaceId.empty()) return; Widgets::MWSkillPtr skillWidget; const int lineHeight = Settings::gui().mFontSize + 2; MyGUI::IntCoord coord1(0, 0, mSkillList->getWidth(), 18); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Race* race = store.get().find(mCurrentRaceId); for (const auto& bonus : race->mData.mBonus) { 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); skillWidget->setSkillId(skill); skillWidget->setSkillValue(Widgets::MWSkill::SkillValue(static_cast(bonus.mBonus), 0.f)); ToolTips::createSkillToolTip(skillWidget, skill); mSkillItems.push_back(skillWidget); coord1.top += lineHeight; } } void RaceDialog::updateSpellPowers() { for (MyGUI::Widget* widget : mSpellPowerItems) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSpellPowerItems.clear(); if (mCurrentRaceId.empty()) return; const int lineHeight = Settings::gui().mFontSize + 2; MyGUI::IntCoord coord(0, 0, mSpellPowerList->getWidth(), lineHeight); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Race* race = store.get().find(mCurrentRaceId); int i = 0; 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)); spellPowerWidget->setSpellId(spellpower); spellPowerWidget->setUserString("ToolTipType", "Spell"); spellPowerWidget->setUserString("Spell", spellpower.serialize()); mSpellPowerItems.push_back(spellPowerWidget); coord.top += lineHeight; ++i; } } const ESM::NPC& RaceDialog::getResult() const { return mPreview->getPrototype(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/race.hpp000066400000000000000000000061041503074453300214440ustar00rootroot00000000000000#ifndef MWGUI_RACE_H #define MWGUI_RACE_H #include "windowbase.hpp" #include #include namespace MWRender { class RaceSelectionPreview; } namespace ESM { struct NPC; } namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWGui { class RaceDialog : public WindowModal { public: RaceDialog(osg::Group* parent, Resource::ResourceSystem* resourceSystem); enum Gender { GM_Male, GM_Female }; 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 ESM::RefId& raceId); void setGender(Gender gender) { mGenderIndex = gender == GM_Male ? 0 : 1; } void setNextButtonShow(bool shown); void onOpen() override; void onClose() override; bool exit() override { return false; } // Events typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onPreviewScroll(MyGUI::Widget* _sender, int _delta); void onHeadRotate(MyGUI::ScrollBar* _sender, size_t _position); void onSelectPreviousGender(MyGUI::Widget* _sender); void onSelectNextGender(MyGUI::Widget* _sender); void onSelectPreviousFace(MyGUI::Widget* _sender); void onSelectNextFace(MyGUI::Widget* _sender); void onSelectPreviousHair(MyGUI::Widget* _sender); void onSelectNextHair(MyGUI::Widget* _sender); void onSelectRace(MyGUI::ListBox* _sender, size_t _index); void onAccept(MyGUI::ListBox* _sender, size_t _index); void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); private: void updateRaces(); void updateSkills(); void updateSpellPowers(); void updatePreview(); void recountParts(); void getBodyParts(int part, std::vector& out); osg::Group* mParent; Resource::ResourceSystem* mResourceSystem; std::vector mAvailableHeads; std::vector mAvailableHairs; MyGUI::ImageBox* mPreviewImage; MyGUI::ListBox* mRaceList; MyGUI::ScrollBar* mHeadRotate; MyGUI::Widget* mSkillList; std::vector mSkillItems; MyGUI::Widget* mSpellPowerList; std::vector mSpellPowerItems; int mGenderIndex, mFaceIndex, mHairIndex; ESM::RefId mCurrentRaceId; float mCurrentAngle; std::unique_ptr mPreview; std::unique_ptr mPreviewTexture; bool mPreviewDirty; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/recharge.cpp000066400000000000000000000103771503074453300223140ustar00rootroot00000000000000#include "recharge.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.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 "sortfilteritemmodel.hpp" namespace MWGui { 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); mBox->setDisplayMode(ItemChargeView::DisplayMode_EnchantmentCharge); mGemIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Recharge::onSelectItem); } void Recharge::onOpen() { center(); SortFilterItemModel* model = new SortFilterItemModel(std::make_unique(MWMechanics::getPlayer())); model->setFilter(SortFilterItemModel::Filter_OnlyRechargable); mBox->setModel(model); // Reset scrollbars mBox->resetScrollbars(); } void Recharge::setPtr(const MWWorld::Ptr& item) { if (item.isEmpty() || !item.getClass().isItem(item)) throw std::runtime_error("Invalid argument in Recharge::setPtr"); mGemIcon->setItem(item); mGemIcon->setUserString("ToolTipType", "ItemPtr"); mGemIcon->setUserData(MWWorld::Ptr(item)); updateView(); } void Recharge::updateView() { MWWorld::Ptr gem = *mGemIcon->getUserData(); const ESM::RefId& soul = gem.getCellRef().getSoul(); const ESM::Creature* creature = MWBase::Environment::get().getESMStore()->get().find(soul); mChargeLabel->setCaptionWithReplacing("#{sCharges} " + MyGUI::utility::toString(creature->mData.mSoul)); bool toolBoxVisible = gem.getCellRef().getCount() != 0; mGemBox->setVisible(toolBoxVisible); mGemBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); if (!toolBoxVisible) { mGemIcon->setItem(MWWorld::Ptr()); mGemIcon->clearUserStrings(); } mBox->update(); Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } void Recharge::onCancel(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Recharge); } 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); } void Recharge::onItemSelected(MWWorld::Ptr item) { mItemSelectionDialog->setVisible(false); mGemIcon->setItem(item); mGemIcon->setUserString("ToolTipType", "ItemPtr"); mGemIcon->setUserData(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateView(); } void Recharge::onItemCancel() { mItemSelectionDialog->setVisible(false); } void Recharge::onItemClicked(MyGUI::Widget* sender, const MWWorld::Ptr& item) { MWWorld::Ptr gem = *mGemIcon->getUserData(); if (!MWMechanics::rechargeItem(item, gem)) return; updateView(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/recharge.hpp000066400000000000000000000021321503074453300223070ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_RECHARGE_H #define OPENMW_MWGUI_RECHARGE_H #include #include "windowbase.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class ItemSelectionDialog; class ItemWidget; class ItemChargeView; class Recharge : public WindowBase { public: Recharge(); void onOpen() override; void setPtr(const MWWorld::Ptr& gem) override; std::string_view getWindowIdForLua() const override { return "Recharge"; } protected: ItemChargeView* mBox; MyGUI::Widget* mGemBox; ItemWidget* mGemIcon; std::unique_ptr mItemSelectionDialog; MyGUI::TextBox* mChargeLabel; MyGUI::Button* mCancelButton; void updateView(); void onSelectItem(MyGUI::Widget* sender); 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/referenceinterface.cpp000066400000000000000000000007041503074453300243440ustar00rootroot00000000000000#include "referenceinterface.hpp" namespace MWGui { ReferenceInterface::ReferenceInterface() = default; ReferenceInterface::~ReferenceInterface() = default; void ReferenceInterface::checkReferenceAvailable() { // check if count of the reference has become 0 if (!mPtr.isEmpty() && mPtr.getCellRef().getCount() == 0) { mPtr = MWWorld::Ptr(); onReferenceUnavailable(); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/referenceinterface.hpp000066400000000000000000000017171503074453300243560ustar00rootroot00000000000000#ifndef MWGUI_REFERENCEINTERFACE_H #define MWGUI_REFERENCEINTERFACE_H #include "../mwworld/ptr.hpp" 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 class ReferenceInterface { public: ReferenceInterface(); virtual ~ReferenceInterface(); void checkReferenceAvailable(); ///< closes the window, if the MW-reference has become unavailable virtual void resetReference() { mPtr = MWWorld::Ptr(); } protected: virtual void onReferenceUnavailable() = 0; ///< called when reference has become unavailable MWWorld::Ptr mPtr; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/repair.cpp000066400000000000000000000111671503074453300220140ustar00rootroot00000000000000#include "repair.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/class.hpp" #include "inventoryitemmodel.hpp" #include "itemchargeview.hpp" #include "itemselection.hpp" #include "itemwidget.hpp" #include "sortfilteritemmodel.hpp" namespace MWGui { Repair::Repair() : WindowBase("openmw_repair.layout") { 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); mRepairBox->eventItemClicked += MyGUI::newDelegate(this, &Repair::onRepairItem); mRepairBox->setDisplayMode(ItemChargeView::DisplayMode_Health); mToolIcon->eventMouseButtonClick += MyGUI::newDelegate(this, &Repair::onSelectItem); } 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(); } void Repair::setPtr(const MWWorld::Ptr& item) { if (item.isEmpty() || !item.getClass().isItem(item)) throw std::runtime_error("Invalid argument in Repair::setPtr"); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Repair Up")); mRepair.setTool(item); mToolIcon->setItem(item); mToolIcon->setUserString("ToolTipType", "ItemPtr"); mToolIcon->setUserData(MWWorld::Ptr(item)); updateRepairView(); } void Repair::updateRepairView() { MWWorld::LiveCellRef* ref = mRepair.getTool().get(); int uses = mRepair.getTool().getClass().getItemHealth(mRepair.getTool()); float quality = ref->mBase->mData.mQuality; mToolIcon->setUserData(mRepair.getTool()); std::stringstream qualityStr; qualityStr << std::setprecision(3) << quality; mUsesLabel->setCaptionWithReplacing("#{sUses} " + MyGUI::utility::toString(uses)); mQualityLabel->setCaptionWithReplacing("#{sQuality} " + qualityStr.str()); bool toolBoxVisible = (mRepair.getTool().getCellRef().getCount() != 0); mToolBox->setVisible(toolBoxVisible); mToolBox->setUserString("Hidden", toolBoxVisible ? "false" : "true"); if (!toolBoxVisible) { mToolIcon->setItem(MWWorld::Ptr()); mToolIcon->clearUserStrings(); } mRepairBox->update(); Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } 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); mToolIcon->setItem(item); mToolIcon->setUserString("ToolTipType", "ItemPtr"); mToolIcon->setUserData(item); mRepair.setTool(item); MWBase::Environment::get().getWindowManager()->playSound(item.getClass().getDownSoundId(item)); updateRepairView(); } void Repair::onItemCancel() { mItemSelectionDialog->setVisible(false); } 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().getCellRef().getCount()) return; mRepair.repair(ptr); updateRepairView(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/repair.hpp000066400000000000000000000021531503074453300220140ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_REPAIR_H #define OPENMW_MWGUI_REPAIR_H #include #include "windowbase.hpp" #include "../mwmechanics/repair.hpp" namespace MWGui { class ItemSelectionDialog; class ItemWidget; class ItemChargeView; class Repair : public WindowBase { public: Repair(); void onOpen() override; void setPtr(const MWWorld::Ptr& item) override; std::string_view getWindowIdForLua() const override { return "Repair"; } protected: ItemChargeView* mRepairBox; MyGUI::Widget* mToolBox; ItemWidget* mToolIcon; std::unique_ptr mItemSelectionDialog; MyGUI::TextBox* mUsesLabel; MyGUI::TextBox* mQualityLabel; MyGUI::Button* mCancelButton; MWMechanics::Repair mRepair; void updateRepairView(); void onSelectItem(MyGUI::Widget* sender); void onItemSelected(MWWorld::Ptr item); void onItemCancel(); void onRepairItem(MyGUI::Widget* sender, const MWWorld::Ptr& ptr); void onCancel(MyGUI::Widget* sender); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/resourceskin.cpp000066400000000000000000000054401503074453300232430ustar00rootroot00000000000000#include "resourceskin.hpp" #include #include namespace MWGui { void resizeSkin(MyGUI::xml::ElementPtr _node) { _node->setAttribute("type", "ResourceSkin"); if (!_node->findAttribute("size").empty()) return; auto textureName = _node->findAttribute("texture"); if (textureName.empty()) return; 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); _node->addAttribute("size", textureSize); while (basis.next()) { if (basis->getName() != "BasisSkin") continue; auto basisSkinType = basis->findAttribute("type"); if (Misc::StringUtils::ciEqual(basisSkinType, "SimpleText")) continue; bool isTileRect = Misc::StringUtils::ciEqual(basisSkinType, "TileRect"); if (!basis->findAttribute("offset").empty()) continue; basis->addAttribute("offset", coord); MyGUI::xml::ElementEnumerator state = basis->getElementEnumerator(); while (state.next()) { if (state->getName() == "State") { if (!state->findAttribute("offset").empty()) continue; state->addAttribute("offset", coord); if (isTileRect) { MyGUI::xml::ElementEnumerator property = state->getElementEnumerator(); bool hasTileSize = false; while (property.next("Property")) { if (property->findAttribute("key") != "TileSize") continue; hasTileSize = true; } if (!hasTileSize) { MyGUI::xml::ElementPtr tileSizeProperty = state->createChild("Property"); tileSizeProperty->addAttribute("key", "TileSize"); tileSizeProperty->addAttribute("value", textureSize); } } } } } } void AutoSizedResourceSkin::deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) { resizeSkin(_node); Base::deserialization(_node, _version); } } openmw-openmw-0.49.0/apps/openmw/mwgui/resourceskin.hpp000066400000000000000000000005461503074453300232520ustar00rootroot00000000000000#ifndef MWGUI_RESOURCESKIN_H #define MWGUI_RESOURCESKIN_H #include namespace MWGui { class AutoSizedResourceSkin final : public MyGUI::ResourceSkin { MYGUI_RTTI_DERIVED(AutoSizedResourceSkin) public: void deserialization(MyGUI::xml::ElementPtr _node, MyGUI::Version _version) override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/review.cpp000066400000000000000000000463721503074453300220410ustar00rootroot00000000000000#include "review.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/autocalcspell.hpp" #include "../mwworld/esmstore.hpp" #include "tooltips.hpp" namespace { void adjustButtonSize(MyGUI::Button* button) { // adjust size of button to fit its text MyGUI::IntSize size = button->getTextSize(); button->setSize(size.width + 24, button->getSize().height); } } namespace MWGui { ReviewDialog::ReviewDialog() : WindowModal("openmw_chargen_review.layout") , mUpdateSkillArea(false) { // Centre dialog center(); // Setup static stats MyGUI::Button* button; getWidget(mNameWidget, "NameText"); getWidget(button, "NameButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onNameClicked); getWidget(mRaceWidget, "RaceText"); getWidget(button, "RaceButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onRaceClicked); getWidget(mClassWidget, "ClassText"); getWidget(button, "ClassButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onClassClicked); getWidget(mBirthSignWidget, "SignText"); getWidget(button, "SignButton"); adjustButtonSize(button); button->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBirthSignClicked); // Setup dynamic stats getWidget(mHealth, "Health"); mHealth->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sHealth", {})); mHealth->setValue(45, 45); getWidget(mMagicka, "Magicka"); mMagicka->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sMagic", {})); mMagicka->setValue(50, 50); getWidget(mFatigue, "Fatigue"); mFatigue->setTitle(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFatigue", {})); mFatigue->setValue(160, 160); // Setup attributes 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) { 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 (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { mSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); mSkillWidgetMap.emplace(skill.mId, static_cast(nullptr)); } MyGUI::Button* backButton; getWidget(backButton, "BackButton"); backButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onBackClicked); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ReviewDialog::onOkClicked); } void ReviewDialog::onOpen() { WindowModal::onOpen(); mUpdateSkillArea = true; } void ReviewDialog::onFrame(float /*duration*/) { if (mUpdateSkillArea) { updateSkillArea(); mUpdateSkillArea = false; } } void ReviewDialog::setPlayerName(const std::string& name) { mNameWidget->setCaption(name); } void ReviewDialog::setRace(const ESM::RefId& raceId) { mRaceId = raceId; const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().search(mRaceId); if (race) { ToolTips::createRaceToolTip(mRaceWidget, race); mRaceWidget->setCaption(race->mName); } mUpdateSkillArea = true; } void ReviewDialog::setClass(const ESM::Class& playerClass) { mClass = playerClass; mClassWidget->setCaption(mClass.mName); ToolTips::createClassToolTip(mClassWidget, mClass); } void ReviewDialog::setBirthSign(const ESM::RefId& signId) { mBirthSignId = signId; const ESM::BirthSign* sign = MWBase::Environment::get().getESMStore()->get().search(mBirthSignId); if (sign) { mBirthSignWidget->setCaption(sign->mName); ToolTips::createBirthsignToolTip(mBirthSignWidget, mBirthSignId); } mUpdateSkillArea = true; } void ReviewDialog::setHealth(const MWMechanics::DynamicStat& value) { int current = std::max(0, static_cast(value.getCurrent())); int modified = static_cast(value.getModified()); mHealth->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mHealth->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } void ReviewDialog::setMagicka(const MWMechanics::DynamicStat& value) { int current = std::max(0, static_cast(value.getCurrent())); int modified = static_cast(value.getModified()); mMagicka->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mMagicka->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } void ReviewDialog::setFatigue(const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified()); mFatigue->setValue(current, modified); std::string valStr = MyGUI::utility::toString(current) + " / " + MyGUI::utility::toString(modified); mFatigue->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } void ReviewDialog::setAttribute(ESM::RefId attributeId, const MWMechanics::AttributeValue& value) { auto attr = mAttributeWidgets.find(attributeId); if (attr == mAttributeWidgets.end()) return; if (attr->second->getAttributeValue() != value) { attr->second->setAttributeValue(value); mUpdateSkillArea = true; } } void ReviewDialog::setSkillValue(ESM::RefId id, const MWMechanics::SkillValue& value) { mSkillValues[id] = value; MyGUI::TextBox* widget = mSkillWidgetMap[id]; if (widget) { float modified = value.getModified(); float base = value.getBase(); std::string text = MyGUI::utility::toString(std::floor(modified)); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; widget->setCaption(text); widget->_setWidgetState(state); } mUpdateSkillArea = true; } 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::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); for (const ESM::Skill& skill : store) { if (!skillSet.contains(skill.mId)) mMiscSkills.push_back(skill.mId); } mUpdateSkillArea = true; } 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); separator->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(separator); coord1.top += separator->getHeight(); coord2.top += separator->getHeight(); } 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); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); groupWidget->setCaption(MyGUI::UString(label)); mSkillWidgets.push_back(groupWidget); const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } 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(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); skillValueWidget = mSkillView->createWidget("SandTextRight", coord2, MyGUI::Align::Default); skillValueWidget->setCaption(value); skillValueWidget->_setWidgetState(state); skillValueWidget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); 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) { MyGUI::TextBox* skillNameWidget; 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); 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); widget->setSpellId(spell->mId); widget->setUserString("ToolTipType", "Spell"); widget->setUserString("Spell", spell->mId.serialize()); widget->eventMouseWheel += MyGUI::newDelegate(this, &ReviewDialog::onMouseWheel); mSkillWidgets.push_back(widget); const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } 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()) { addSeparator(coord1, coord2); } addGroup( MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); for (const ESM::RefId& skillId : skills) { 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; } const MWMechanics::SkillValue& stat = skillValue->second; int base = stat.getBase(); int modified = stat.getModified(); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; MyGUI::TextBox* widget = addValueItem( skill->mName, MyGUI::utility::toString(static_cast(modified)), state, coord1, coord2); for (int i = 0; i < 2; ++i) { ToolTips::createSkillToolTip(mSkillWidgets[mSkillWidgets.size() - 1 - i], skill->mId); } mSkillWidgetMap[skill->mId] = widget; } } void ReviewDialog::updateSkillArea() { for (MyGUI::Widget* skillWidget : mSkillWidgets) { MyGUI::Gui::getInstance().destroyWidget(skillWidget); } mSkillWidgets.clear(); const int valueSize = 40; MyGUI::IntCoord coord1(10, 0, mSkillView->getWidth() - (10 + valueSize) - 24, 18); MyGUI::IntCoord coord2(coord1.left + coord1.width, coord1.top, valueSize, coord1.height); if (!mMajorSkills.empty()) addSkills(mMajorSkills, "sSkillClassMajor", "Major Skills", coord1, coord2); if (!mMinorSkills.empty()) addSkills(mMinorSkills, "sSkillClassMinor", "Minor Skills", coord1, coord2); if (!mMiscSkills.empty()) addSkills(mMiscSkills, "sSkillClassMisc", "Misc Skills", coord1, coord2); // starting spells std::vector spells; const ESM::Race* race = nullptr; if (!mRaceId.empty()) race = MWBase::Environment::get().getESMStore()->get().find(mRaceId); std::map attributes; for (const auto& [key, value] : mAttributeWidgets) attributes[key] = value->getAttributeValue(); std::vector selectedSpells = MWMechanics::autoCalcPlayerSpells(mSkillValues, attributes, race); for (ESM::RefId& spellId : selectedSpells) { if (std::find(spells.begin(), spells.end(), spellId) == spells.end()) spells.push_back(spellId); } if (race) { for (const ESM::RefId& spellId : race->mPowers.mList) { if (std::find(spells.begin(), spells.end(), spellId) == spells.end()) spells.push_back(spellId); } } if (!mBirthSignId.empty()) { const ESM::BirthSign* sign = MWBase::Environment::get().getESMStore()->get().find(mBirthSignId); for (const auto& spellId : sign->mPowers.mList) { 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 (auto& spellId : spells) { 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 (auto& spellId : spells) { 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 (auto& spellId : spells) { 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 mSkillView->setVisibleVScroll(false); mSkillView->setCanvasSize(mSkillView->getWidth(), std::max(mSkillView->getHeight(), coord1.top)); mSkillView->setVisibleVScroll(true); } // widget controls void ReviewDialog::onOkClicked(MyGUI::Widget* _sender) { eventDone(this); } void ReviewDialog::onBackClicked(MyGUI::Widget* _sender) { eventBack(); } void ReviewDialog::onNameClicked(MyGUI::Widget* _sender) { eventActivateDialog(NAME_DIALOG); } void ReviewDialog::onRaceClicked(MyGUI::Widget* _sender) { eventActivateDialog(RACE_DIALOG); } void ReviewDialog::onClassClicked(MyGUI::Widget* _sender) { eventActivateDialog(CLASS_DIALOG); } void ReviewDialog::onBirthSignClicked(MyGUI::Widget* _sender) { eventActivateDialog(BIRTHSIGN_DIALOG); } void ReviewDialog::onMouseWheel(MyGUI::Widget* _sender, int _rel) { 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))); } } openmw-openmw-0.49.0/apps/openmw/mwgui/review.hpp000066400000000000000000000070731503074453300220410ustar00rootroot00000000000000#ifndef MWGUI_REVIEW_H #define MWGUI_REVIEW_H #include "widgets.hpp" #include "windowbase.hpp" #include #include namespace ESM { struct Spell; } namespace MWGui { class ReviewDialog : public WindowModal { public: enum Dialogs { NAME_DIALOG, RACE_DIALOG, CLASS_DIALOG, BIRTHSIGN_DIALOG }; ReviewDialog(); bool exit() override { return false; } 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::RefId attributeId, const MWMechanics::AttributeValue& 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::MultiDelegate<> EventHandle_Void; typedef MyGUI::delegates::MultiDelegate EventHandle_Int; /** Event : Back button clicked.\n signature : void method()\n */ EventHandle_Void eventBack; /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; EventHandle_Int eventActivateDialog; protected: void onOkClicked(MyGUI::Widget* _sender); void onBackClicked(MyGUI::Widget* _sender); void onNameClicked(MyGUI::Widget* _sender); void onRaceClicked(MyGUI::Widget* _sender); void onClassClicked(MyGUI::Widget* _sender); void onBirthSignClicked(MyGUI::Widget* _sender); void onMouseWheel(MyGUI::Widget* _sender, int _rel); 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); 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; MyGUI::ScrollView* mSkillView; Widgets::MWDynamicStatPtr mHealth, mMagicka, mFatigue; std::map mAttributeWidgets; 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/savegamedialog.cpp000066400000000000000000000451751503074453300235100ustar00rootroot00000000000000#include "savegamedialog.hpp" #include #include #include #include #include #include #include #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 "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwstate/character.hpp" #include "confirmationdialog.hpp" namespace MWGui { SaveGameDialog::SaveGameDialog() : WindowModal("openmw_savegame_dialog.layout") , mSaving(true) , mCurrentCharacter(nullptr) , mCurrentSlot(nullptr) { getWidget(mScreenshot, "Screenshot"); getWidget(mCharacterSelection, "SelectCharacter"); getWidget(mInfoText, "InfoText"); getWidget(mOkButton, "OkButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mSaveList, "SaveList"); getWidget(mSaveNameEdit, "SaveNameEdit"); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onOkButtonClicked); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onCancelButtonClicked); mDeleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteButtonClicked); mCharacterSelection->eventComboChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterSelected); mCharacterSelection->eventComboAccept += MyGUI::newDelegate(this, &SaveGameDialog::onCharacterAccept); mSaveList->eventListChangePosition += MyGUI::newDelegate(this, &SaveGameDialog::onSlotSelected); mSaveList->eventListMouseItemActivate += MyGUI::newDelegate(this, &SaveGameDialog::onSlotMouseClick); mSaveList->eventListSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onSlotActivated); mSaveList->eventKeyButtonPressed += MyGUI::newDelegate(this, &SaveGameDialog::onKeyButtonPressed); mSaveNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SaveGameDialog::onEditSelectAccept); mSaveNameEdit->eventEditTextChange += MyGUI::newDelegate(this, &SaveGameDialog::onSaveNameChanged); // To avoid accidental deletions mDeleteButton->setNeedKeyFocus(false); } void SaveGameDialog::onSlotActivated(MyGUI::ListBox* sender, size_t pos) { onSlotSelected(sender, pos); accept(); } void SaveGameDialog::onSlotMouseClick(MyGUI::ListBox* sender, size_t pos) { onSlotSelected(sender, pos); if (pos != MyGUI::ITEM_NONE && MyGUI::InputManager::getInstance().isShiftPressed()) confirmDeleteSave(); } void SaveGameDialog::confirmDeleteSave() { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:DeleteGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotConfirmed); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onDeleteSlotCancel); } void SaveGameDialog::onDeleteSlotConfirmed() { MWBase::Environment::get().getStateManager()->deleteGame(mCurrentCharacter, mCurrentSlot); mSaveList->removeItemAt(mSaveList->getIndexSelected()); onSlotSelected(mSaveList, mSaveList->getIndexSelected()); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); if (mSaveList->getItemCount() == 0) { size_t previousIndex = mCharacterSelection->getIndexSelected(); mCurrentCharacter = nullptr; mCharacterSelection->removeItemAt(previousIndex); if (mCharacterSelection->getItemCount()) { size_t nextCharacter = std::min(previousIndex, mCharacterSelection->getItemCount() - 1); mCharacterSelection->setIndexSelected(nextCharacter); onCharacterSelected(mCharacterSelection, nextCharacter); } else mCharacterSelection->setIndexSelected(MyGUI::ITEM_NONE); } } void SaveGameDialog::onDeleteSlotCancel() { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } 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) { accept(); // To do not spam onEditSelectAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void SaveGameDialog::onClose() { mSaveList->setIndexSelected(MyGUI::ITEM_NONE); WindowModal::onClose(); } void SaveGameDialog::onOpen() { WindowModal::onOpen(); mSaveNameEdit->setCaption({}); if (mSaving) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveNameEdit); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); center(); mCharacterSelection->setCaption({}); mCharacterSelection->removeAllItems(); mCurrentCharacter = nullptr; mCurrentSlot = nullptr; mSaveList->removeAllItems(); onSlotSelected(mSaveList, MyGUI::ITEM_NONE); MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); if (mgr->characterBegin() == mgr->characterEnd()) return; mCurrentCharacter = mgr->getCurrentCharacter(); 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()) { const ESM::SavedGame& signature = it->getSignature(); std::stringstream title; 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_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().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 << " (#{OMWEngine:Level} " << signature.mPlayerLevel << " " << MyGUI::TextIterator::toTagsString(MyGUI::UString(className)) << ")"; mCharacterSelection->addItem(MyGUI::LanguageManager::getInstance().replaceTags(title.str())); if (mCurrentCharacter == &*it || (!mCurrentCharacter && !mSaving && Misc::StringUtils::ciEqual( directory, Files::pathToUnicodeString(it->begin()->mPath.parent_path().filename())))) { mCurrentCharacter = &*it; selectedIndex = mCharacterSelection->getItemCount() - 1; } } } mCharacterSelection->setIndexSelected(selectedIndex); if (selectedIndex == MyGUI::ITEM_NONE) mCharacterSelection->setCaptionWithReplacing("#{OMWEngine:SelectCharacter}"); fillSaveList(); } void SaveGameDialog::setLoadOrSave(bool load) { mSaving = !load; mSaveNameEdit->setVisible(!load); mCharacterSelection->setUserString("Hidden", load ? "false" : "true"); mCharacterSelection->setVisible(load); mDeleteButton->setUserString("Hidden", load ? "false" : "true"); mDeleteButton->setVisible(load); if (!load) { mCurrentCharacter = MWBase::Environment::get().getStateManager()->getCurrentCharacter(); } center(); } void SaveGameDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); } void SaveGameDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { if (mCurrentSlot) confirmDeleteSave(); } void SaveGameDialog::onConfirmationGiven() { accept(true); } void SaveGameDialog::onConfirmationCancel() { MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } void SaveGameDialog::accept(bool reallySure) { if (mSaving) { // If overwriting an existing slot, ask for confirmation first if (mCurrentSlot != nullptr && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:OverwriteGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationCancel); return; } if (mSaveNameEdit->getCaption().empty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{OMWEngine:EmptySaveNameError}"); return; } } else { MWBase::StateManager::State state = MWBase::Environment::get().getStateManager()->getState(); // If game is running, ask for confirmation first if (state == MWBase::StateManager::State_Running && !reallySure) { ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:LoadGameConfirmation}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationGiven); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SaveGameDialog::onConfirmationCancel); return; } } setVisible(false); MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); if (mSaving) { MWBase::Environment::get().getStateManager()->saveGame(mSaveNameEdit->getCaption(), mCurrentSlot); } else { assert(mCurrentCharacter && mCurrentSlot); MWBase::Environment::get().getStateManager()->loadGame(mCurrentCharacter, mCurrentSlot->mPath); } } void SaveGameDialog::onKeyButtonPressed(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char character) { if (key == MyGUI::KeyCode::Delete && mCurrentSlot) confirmDeleteSave(); } void SaveGameDialog::onOkButtonClicked(MyGUI::Widget* sender) { accept(); } void SaveGameDialog::onCharacterSelected(MyGUI::ComboBox* sender, size_t pos) { MWBase::StateManager* mgr = MWBase::Environment::get().getStateManager(); unsigned int i = 0; const MWState::Character* character = nullptr; for (MWBase::StateManager::CharacterIterator it = mgr->characterBegin(); it != mgr->characterEnd(); ++it, ++i) { if (i == pos) character = &*it; } assert(character && "Can't find selected character"); mCurrentCharacter = character; mCurrentSlot = nullptr; fillSaveList(); } void SaveGameDialog::onCharacterAccept(MyGUI::ComboBox* sender, size_t pos) { // Give key focus to save list so we can confirm the selection with Enter MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mSaveList); } void SaveGameDialog::fillSaveList() { mSaveList->removeAllItems(); if (!mCurrentCharacter) return; for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it) { mSaveList->addItem(it->mProfile.mDescription); } // When loading, Auto-select the first save, if there is one if (mSaveList->getItemCount() && !mSaving) { mSaveList->setIndexSelected(0); onSlotSelected(mSaveList, 0); } else onSlotSelected(mSaveList, MyGUI::ITEM_NONE); } std::string formatTimeplayed(const double timeInSeconds) { 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) { mOkButton->setEnabled(pos != MyGUI::ITEM_NONE || mSaving); mDeleteButton->setEnabled(pos != MyGUI::ITEM_NONE); if (pos == MyGUI::ITEM_NONE || !mCurrentCharacter) { mCurrentSlot = nullptr; mInfoText->setCaption({}); mScreenshot->setImageTexture({}); return; } if (mSaving) mSaveNameEdit->setCaption(sender->getItemNameAt(pos)); mCurrentSlot = nullptr; unsigned int i = 0; for (MWState::Character::SlotIterator it = mCurrentCharacter->begin(); it != mCurrentCharacter->end(); ++it, ++i) { if (i == pos) mCurrentSlot = &*it; } if (!mCurrentSlot) throw std::runtime_error("Can't find selected slot"); std::stringstream text; text << Misc::fileTimeToString(mCurrentSlot->mTimeStamp, "%Y.%m.%d %T") << "\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 (mCurrentSlot->mProfile.mCurrentDay > 0) text << "#{Calendar:day} " << mCurrentSlot->mProfile.mCurrentDay << "\n"; text << mCurrentSlot->mProfile.mInGameTime.mDay << " " << MWBase::Environment::get().getWorld()->getTimeManager()->getMonthName( mCurrentSlot->mProfile.mInGameTime.mMonth) << " " << hour << " " << (pm ? "#{Calendar:pm}" : "#{Calendar:am}"); if (mCurrentSlot->mProfile.mTimePlayed > 0) { 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; 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) << "Can't open savegame screenshot, no jpg readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(instream); if (!result.success()) { Log(Debug::Error) << "Failed to read savegame screenshot: " << result.message() << " code " << result.status(); return; } 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); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setResizeNonPowerOfTwoHint(false); texture->setUnRefImageDataAfterApply(true); mScreenshotTexture = std::make_unique(texture); mScreenshot->setRenderItemTexture(mScreenshotTexture.get()); } } openmw-openmw-0.49.0/apps/openmw/mwgui/savegamedialog.hpp000066400000000000000000000040011503074453300234740ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_SAVEGAMEDIALOG_H #define OPENMW_MWGUI_SAVEGAMEDIALOG_H #include #include "windowbase.hpp" namespace MWState { class Character; struct Slot; } namespace MWGui { class SaveGameDialog : public MWGui::WindowModal { public: SaveGameDialog(); void onOpen() override; void onClose() override; void setLoadOrSave(bool load); private: 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 onCharacterAccept(MyGUI::ComboBox* sender, size_t pos); // Slot selected (mouse click or arrow keys) void onSlotSelected(MyGUI::ListBox* sender, size_t pos); // Slot activated (double click or enter key) 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 onConfirmationGiven(); void onConfirmationCancel(); void accept(bool reallySure = false); void fillSaveList(); std::unique_ptr mScreenshotTexture; MyGUI::ImageBox* mScreenshot; bool mSaving; MyGUI::ComboBox* mCharacterSelection; MyGUI::EditBox* mInfoText; MyGUI::Button* mOkButton; MyGUI::Button* mCancelButton; MyGUI::Button* mDeleteButton; MyGUI::ListBox* mSaveList; MyGUI::EditBox* mSaveNameEdit; const MWState::Character* mCurrentCharacter; const MWState::Slot* mCurrentSlot; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/screenfader.cpp000066400000000000000000000121561503074453300230120ustar00rootroot00000000000000#include "screenfader.hpp" #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) { } bool FadeOp::isRunning() { return mRunning; } void FadeOp::start() { if (mRunning) return; mRemainingTime = mTargetTime + mDelay; mStartAlpha = mFader->getCurrentAlpha(); mRunning = true; } void FadeOp::update(float dt) { if (!mRunning) return; if (mStartAlpha == mTargetAlpha) { finish(); return; } if (mRemainingTime <= 0) { // Make sure the target alpha is applied mFader->notifyAlphaChanged(mTargetAlpha); finish(); return; } if (mRemainingTime > mTargetTime) { mRemainingTime -= dt; return; } float currentAlpha = mFader->getCurrentAlpha(); if (mStartAlpha > mTargetAlpha) { currentAlpha -= dt / mTargetTime * (mStartAlpha - mTargetAlpha); if (currentAlpha < mTargetAlpha) currentAlpha = mTargetAlpha; } else { currentAlpha += dt / mTargetTime * (mTargetAlpha - mStartAlpha); if (currentAlpha > mTargetAlpha) currentAlpha = mTargetAlpha; } mFader->notifyAlphaChanged(currentAlpha); mRemainingTime -= dt; } void FadeOp::finish() { mRunning = false; mFader->notifyOperationFinished(); } ScreenFader::ScreenFader( const std::string& texturePath, const std::string& layout, const MyGUI::FloatCoord& texCoordOverride) : WindowBase(layout) , mCurrentAlpha(0.f) , mFactor(1.f) , mRepeat(false) { MyGUI::Gui::getInstance().eventFrameStart += MyGUI::newDelegate(this, &ScreenFader::onFrameStart); MyGUI::ImageBox* imageBox = mMainWidget->castType(false); if (imageBox) { imageBox->setImageTexture(texturePath); const MyGUI::IntSize imageSize = imageBox->getImageSize(); 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))); } } ScreenFader::~ScreenFader() { try { MyGUI::Gui::getInstance().eventFrameStart -= MyGUI::newDelegate(this, &ScreenFader::onFrameStart); } catch (const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void ScreenFader::onFrameStart(float dt) { if (!mQueue.empty()) { if (!mQueue.front()->isRunning()) mQueue.front()->start(); mQueue.front()->update(dt); } } void ScreenFader::applyAlpha() { setVisible(true); mMainWidget->setAlpha(1.f - ((1.f - mCurrentAlpha) * mFactor)); } void ScreenFader::fadeIn(float time, float delay) { queue(time, 1.f, delay); } void ScreenFader::fadeOut(const float time, float delay) { queue(time, 0.f, delay); } void ScreenFader::fadeTo(const int percent, const float time, float delay) { queue(time, percent / 100.f, delay); } void ScreenFader::clear() { clearQueue(); notifyAlphaChanged(0.f); } void ScreenFader::setFactor(float factor) { mFactor = factor; applyAlpha(); } void ScreenFader::setRepeat(bool repeat) { mRepeat = repeat; } void ScreenFader::queue(float time, float targetAlpha, float delay) { if (time < 0.f) return; if (time == 0.f && delay == 0.f) { mCurrentAlpha = targetAlpha; applyAlpha(); return; } mQueue.push_back(FadeOp::Ptr(new FadeOp(this, time, targetAlpha, delay))); } bool ScreenFader::isEmpty() { return mQueue.empty(); } void ScreenFader::clearQueue() { mQueue.clear(); } void ScreenFader::notifyAlphaChanged(float alpha) { if (mCurrentAlpha == alpha) return; mCurrentAlpha = alpha; if (1.f - ((1.f - mCurrentAlpha) * mFactor) == 0.f) mMainWidget->setVisible(false); else applyAlpha(); } void ScreenFader::notifyOperationFinished() { FadeOp::Ptr op = mQueue.front(); mQueue.pop_front(); if (mRepeat) mQueue.push_back(op); } float ScreenFader::getCurrentAlpha() { return mCurrentAlpha; } } openmw-openmw-0.49.0/apps/openmw/mwgui/screenfader.hpp000066400000000000000000000032761503074453300230220ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_SCREENFADER_H #define OPENMW_MWGUI_SCREENFADER_H #include #include #include "windowbase.hpp" namespace MWGui { class ScreenFader; class FadeOp { public: typedef std::shared_ptr Ptr; FadeOp(ScreenFader* fader, float time, float targetAlpha, float delay); bool isRunning(); void start(); void update(float dt); void finish(); private: ScreenFader* mFader; float mRemainingTime; float mTargetTime; float mTargetAlpha; float mStartAlpha; float mDelay; bool mRunning; }; 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(); 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 clear() override; void setFactor(float factor); void setRepeat(bool repeat); void queue(float time, float targetAlpha, float delay); bool isEmpty(); void clearQueue(); void notifyAlphaChanged(float alpha); void notifyOperationFinished(); float getCurrentAlpha(); private: void applyAlpha(); float mCurrentAlpha; float mFactor; bool mRepeat; // repeat queued operations without removing them std::deque mQueue; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/scrollwindow.cpp000066400000000000000000000100561503074453300232540ustar00rootroot00000000000000#include "scrollwindow.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwworld/actiontake.hpp" #include "../mwworld/class.hpp" #include "formatting.hpp" namespace MWGui { ScrollWindow::ScrollWindow() : BookWindowBase("openmw_scroll.layout") , mTakeButtonShow(true) , mTakeButtonAllowed(true) { getWidget(mTextView, "TextView"); getWidget(mCloseButton, "CloseButton"); mCloseButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ScrollWindow::onCloseButtonClicked); getWidget(mTakeButton, "TakeButton"); mTakeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &ScrollWindow::onTakeButtonClicked); adjustButton("CloseButton"); adjustButton("TakeButton"); mCloseButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); mTakeButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &ScrollWindow::onKeyButtonPressed); center(); } 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); 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, *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 mTextView->setVisibleVScroll(false); if (size.height > mTextView->getSize().height) mTextView->setCanvasSize(mTextView->getWidth(), size.height); else mTextView->setCanvasSize(mTextView->getWidth(), mTextView->getSize().height); mTextView->setVisibleVScroll(true); 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) { int scroll = 0; if (key == MyGUI::KeyCode::ArrowUp) scroll = 40; else if (key == MyGUI::KeyCode::ArrowDown) scroll = -40; if (scroll != 0) mTextView->setViewOffset(mTextView->getViewOffset() + MyGUI::IntPoint(0, scroll)); } void ScrollWindow::setTakeButtonShow(bool show) { mTakeButtonShow = show; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void ScrollWindow::setInventoryAllowed(bool allowed) { mTakeButtonAllowed = allowed; mTakeButton->setVisible(mTakeButtonShow && mTakeButtonAllowed); } void ScrollWindow::onCloseButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } void ScrollWindow::onTakeButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Book Up")); MWWorld::ActionTake take(mScroll); take.execute(MWMechanics::getPlayer()); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Scroll); } } openmw-openmw-0.49.0/apps/openmw/mwgui/scrollwindow.hpp000066400000000000000000000020111503074453300232510ustar00rootroot00000000000000#ifndef MWGUI_SCROLLWINDOW_H #define MWGUI_SCROLLWINDOW_H #include "windowbase.hpp" #include "../mwworld/ptr.hpp" namespace Gui { class ImageButton; } namespace MWGui { class ScrollWindow : public BookWindowBase { public: ScrollWindow(); void setPtr(const MWWorld::Ptr& scroll) override; void setInventoryAllowed(bool allowed); void onResChange(int, int) override { center(); } std::string_view getWindowIdForLua() const override { return "Scroll"; } 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); private: Gui::ImageButton* mCloseButton; Gui::ImageButton* mTakeButton; MyGUI::ScrollView* mTextView; MWWorld::Ptr mScroll; bool mTakeButtonShow; bool mTakeButtonAllowed; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/settings.cpp000066400000000000000000000310541503074453300223670ustar00rootroot00000000000000#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, }; } } openmw-openmw-0.49.0/apps/openmw/mwgui/settings.hpp000066400000000000000000000027741503074453300224030ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwgui/settingswindow.cpp000066400000000000000000001271711503074453300236250ustar00rootroot00000000000000#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 "../mwbase/environment.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}"; 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) { 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 MyGUI::LanguageManager::getInstance().replaceTags(result); } 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::trim(split[0]); Misc::StringUtils::trim(split[1]); x = MyGUI::utility::parseInt(split[0]); y = MyGUI::utility::parseInt(split[1]); } bool sortResolutions(std::pair left, std::pair right) { if (left.first == right.first) return left.second > right.second; return left.first > right.first; } const std::string_view checkButtonType = "CheckButton"; const std::string_view sliderType = "Slider"; std::string_view getSettingType(MyGUI::Widget* widget) { return widget->getUserString("SettingType"); } std::string_view getSettingName(MyGUI::Widget* widget) { return widget->getUserString("SettingName"); } std::string_view getSettingCategory(MyGUI::Widget* widget) { return widget->getUserString("SettingCategory"); } std::string_view getSettingValueType(MyGUI::Widget* widget) { return widget->getUserString("SettingValueType"); } void getSettingMinMax(MyGUI::Widget* widget, float& min, float& max) { const char* settingMin = "SettingMin"; const char* settingMax = "SettingMax"; min = 0.f; max = 1.f; if (!widget->getUserString(settingMin).empty()) min = MyGUI::utility::parseFloat(widget->getUserString(settingMin)); if (!widget->getUserString(settingMax).empty()) max = MyGUI::utility::parseFloat(widget->getUserString(settingMax)); } void updateMaxLightsComboBox(MyGUI::ComboBox* box) { constexpr int min = 8; constexpr int max = 64; constexpr int increment = 8; const int maxLights = Settings::shaders().mMaxLights; // show increments of 8 in dropdown if (maxLights >= min && maxLights <= max && !(maxLights % increment)) box->setIndexSelected((maxLights / increment) - 1); else box->setIndexSelected(MyGUI::ITEM_NONE); } } namespace MWGui { void SettingsWindow::configureWidgets(MyGUI::Widget* widget, bool init) { MyGUI::EnumeratorWidgetPtr widgets = widget->getEnumerator(); while (widgets.next()) { MyGUI::Widget* current = widgets.current(); std::string_view type = getSettingType(current); if (type == checkButtonType) { 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); } if (type == sliderType) { MyGUI::ScrollBar* scroll = current->castType(); std::string valueStr; 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; getSettingMinMax(scroll, min, max); float value; if (valueType == "Cell") { value = Settings::get(getSettingCategory(current), getSettingName(current)); std::stringstream ss; 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 { const int intValue = Settings::get(getSettingCategory(current), getSettingName(current)); valueStr = MyGUI::utility::toString(intValue); value = static_cast(intValue); } value = std::clamp(value, min, max); value = (value - min) / (max - min); scroll->setScrollPosition(static_cast(value * (scroll->getScrollRange() - 1))); } else { 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); if (scroll->getVisible()) updateSliderLabel(scroll, valueStr); } configureWidgets(current, init); } } 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) { auto labelWidgetName = scroller->getUserString("SettingLabelWidget"); if (!labelWidgetName.empty()) { MyGUI::TextBox* textBox; getWidget(textBox, labelWidgetName); std::string labelCaption{ scroller->getUserString("SettingLabelCaption") }; labelCaption = Misc::StringUtils::format(labelCaption, value); textBox->setCaptionWithReplacing(labelCaption); } } SettingsWindow::SettingsWindow() : WindowBase("openmw_settings_window.layout") , mKeyboardMode(true) , mCurrentPage(-1) { 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("#{OMWEngine:SettingsWindow}"); getWidget(mSettingsTab, "SettingsTab"); getWidget(mOkButton, "OkButton"); getWidget(mResolutionList, "ResolutionList"); getWidget(mWindowModeList, "WindowModeList"); getWidget(mVSyncModeList, "VSyncModeList"); getWidget(mWindowBorderButton, "WindowBorderButton"); getWidget(mTextureFilteringButton, "TextureFilteringButton"); 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; getWidget(gammaSlider, "GammaSlider"); gammaSlider->setVisible(false); MyGUI::TextBox* textBox; getWidget(textBox, "GammaText"); textBox->setVisible(false); getWidget(textBox, "GammaTextDark"); textBox->setVisible(false); getWidget(textBox, "GammaTextLight"); textBox->setVisible(false); #endif 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); mResolutionList->eventListChangePosition += MyGUI::newDelegate(this, &SettingsWindow::onResolutionSelected); 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); 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); // fill resolution list const int screen = Settings::video().mScreen; int numDisplayModes = SDL_GetNumDisplayModes(screen); std::vector> resolutions; for (int i = 0; i < numDisplayModes; i++) { SDL_DisplayMode mode; SDL_GetDisplayMode(screen, i, &mode); resolutions.emplace_back(mode.w, mode.h); } std::sort(resolutions.begin(), resolutions.end(), sortResolutions); for (std::pair& resolution : resolutions) { 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(); mTextureFilteringButton->setCaptionWithReplacing( textureFilteringToStr(Settings::general().mTextureMipmap, Settings::general().mTextureMinFilter)); int waterTextureSize = Settings::water().mRttSize; if (waterTextureSize >= 512) mWaterTextureSize->setIndexSelected(0); if (waterTextureSize >= 1024) mWaterTextureSize->setIndexSelected(1); if (waterTextureSize >= 2048) mWaterTextureSize->setIndexSelected(2); 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); 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*/) { resetScrollbars(); } void SettingsWindow::onOkButtonClicked(MyGUI::Widget* _sender) { setVisible(false); } void SettingsWindow::onResolutionSelected(MyGUI::ListBox* _sender, size_t index) { if (index == MyGUI::ITEM_NONE) return; ConfirmationDialog* dialog = MWBase::Environment::get().getWindowManager()->getConfirmationDialog(); dialog->askForConfirmation("#{OMWEngine:ConfirmResolution}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResolutionAccept); dialog->eventCancelClicked.clear(); dialog->eventCancelClicked += MyGUI::newDelegate(this, &SettingsWindow::onResolutionCancel); } void SettingsWindow::onResolutionAccept() { 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); apply(); } void SettingsWindow::onResolutionCancel() { highlightCurrentResolution(); } void SettingsWindow::highlightCurrentResolution() { mResolutionList->setIndexSelected(MyGUI::ITEM_NONE); const int currentX = Settings::video().mResolutionX; const int currentY = Settings::video().mResolutionY; for (size_t i = 0; i < mResolutionList->getItemCount(); ++i) { int resX, resY; parseResolution(resX, resY, mResolutionList->getItemNameAt(i)); if (resX == currentX && resY == currentY) { mResolutionList->setIndexSelected(i); break; } } } 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; if (pos == 0) size = 512; else if (pos == 1) size = 1024; else if (pos == 2) size = 2048; Settings::water().mRttSize.set(size); apply(); } void SettingsWindow::onWaterReflectionDetailChanged(MyGUI::ComboBox* _sender, size_t pos) { 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(); } void SettingsWindow::onLightingMethodButtonChanged(MyGUI::ComboBox* _sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; _sender->setCaptionWithReplacing(_sender->getItemNameAt(_sender->getIndexSelected())); MWBase::Environment::get().getWindowManager()->interactiveMessageBox( "#{OMWEngine:ChangeRequiresRestart}", { "#{Interface:OK}" }, true); Settings::shaders().mLightingMethod.set( Settings::parseLightingMethod(*_sender->getItemDataAt(pos))); apply(); } void SettingsWindow::onLanguageChanged(size_t langPriority, MyGUI::ComboBox* _sender, size_t pos) { if (pos == MyGUI::ITEM_NONE) return; _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 = { "#{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; Settings::shaders().mForcePerPixelLighting.reset(); Settings::shaders().mClassicFalloff.reset(); Settings::shaders().mMatchSunlightToSun.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(); configureWidgets(mMainWidget, false); } void SettingsWindow::onButtonToggled(MyGUI::Widget* _sender) { const std::string on = MWBase::Environment::get().getL10nManager()->getMessage("Interface", "On"); const std::string off = MWBase::Environment::get().getL10nManager()->getMessage("Interface", "Off"); bool newState; if (_sender->castType()->getCaption() == on) { _sender->castType()->setCaption(MyGUI::UString(off)); newState = false; } else { _sender->castType()->setCaption(MyGUI::UString(on)); newState = true; } if (getSettingType(_sender) == checkButtonType) { Settings::get(getSettingCategory(_sender), getSettingName(_sender)).set(newState); apply(); return; } } void SettingsWindow::onTextureFilteringChanged(MyGUI::ComboBox* _sender, size_t 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_view valueType = getSettingValueType(scroller); if (valueType == "Float" || valueType == "Integer" || valueType == "Cell") { float value = pos / float(scroller->getScrollRange() - 1); float min, max; getSettingMinMax(scroller, min, max); 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; 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::get(getSettingCategory(scroller), getSettingName(scroller)).set(pos); valueStr = MyGUI::utility::toString(pos); } updateSliderLabel(scroller, valueStr); apply(); } } void SettingsWindow::apply() { const Settings::CategorySettingVector changed = Settings::Manager::getPendingChanges(); MWBase::Environment::get().getWorld()->processChangedSettings(changed); MWBase::Environment::get().getSoundManager()->processChangedSettings(changed); MWBase::Environment::get().getWindowManager()->processChangedSettings(changed); MWBase::Environment::get().getInputManager()->processChangedSettings(changed); MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed); Settings::Manager::resetPendingChanges(); } void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender) { if (mKeyboardMode) return; mKeyboardMode = true; mKeyboardSwitch->setStateSelected(true); mControllerSwitch->setStateSelected(false); updateControlsBox(); resetScrollbars(); } void SettingsWindow::onControllerSwitchClicked(MyGUI::Widget* _sender) { if (!mKeyboardMode) return; mKeyboardMode = false; mKeyboardSwitch->setStateSelected(false); mControllerSwitch->setStateSelected(true); updateControlsBox(); resetScrollbars(); } void SettingsWindow::updateControlsBox() { while (mControlsBox->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mControlsBox->getChildAt(0)); MWBase::Environment::get().getWindowManager()->removeStaticMessageBox(); const auto inputManager = MWBase::Environment::get().getInputManager(); const auto& actions = mKeyboardMode ? inputManager->getActionKeySorting() : inputManager->getActionControllerSorting(); for (const int& action : actions) { std::string desc{ inputManager->getActionDescription(action) }; if (desc.empty()) continue; std::string binding; if (mKeyboardMode) binding = inputManager->getActionKeyBindingName(action); else binding = inputManager->getActionControllerBindingName(action); 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); rightText->setCaptionWithReplacing(binding); 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); Gui::ButtonGroup group; group.push_back(leftText); group.push_back(rightText); Gui::SharedStateButton::createButtonGroup(group); } layoutControlsBox(); } void SettingsWindow::updateLightSettings() { auto lightingMethod = MWBase::Environment::get().getResourceSystem()->getSceneManager()->getLightingMethod(); std::string lightingMethodStr = lightingMethodToStr(lightingMethod); mLightingMethodButton->removeAllItems(); std::array methods = { SceneUtil::LightingMethod::FFP, SceneUtil::LightingMethod::PerObjectUniform, SceneUtil::LightingMethod::SingleUBO, }; for (const auto& method : methods) { if (!MWBase::Environment::get().getResourceSystem()->getSceneManager()->isSupportedLightingMethod(method)) continue; 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 = 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); 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 mControlsBox->setVisibleVScroll(false); 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("#{Interface:None}"); 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) mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); else 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("#{OMWEngine:ConfirmResetBindings}"); dialog->eventOkClicked.clear(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SettingsWindow::onResetDefaultBindingsAccept); dialog->eventCancelClicked.clear(); } void SettingsWindow::onResetDefaultBindingsAccept() { if (mKeyboardMode) MWBase::Environment::get().getInputManager()->resetToDefaultKeyBindings(); else MWBase::Environment::get().getInputManager()->resetToDefaultControllerBindings(); updateControlsBox(); } void SettingsWindow::onOpen() { highlightCurrentResolution(); updateControlsBox(); updateLightSettings(); updateWindowModeSettings(); updateVSyncModeSettings(); resetScrollbars(); renderScriptSettings(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mOkButton); } 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); mControlsBox->setViewOffset(MyGUI::IntPoint(0, 0)); } } openmw-openmw-0.49.0/apps/openmw/mwgui/settingswindow.hpp000066400000000000000000000105211503074453300236200ustar00rootroot00000000000000#ifndef MWGUI_SETTINGS_H #define MWGUI_SETTINGS_H #include #include "windowbase.hpp" namespace MWGui { class SettingsWindow : public WindowBase { public: SettingsWindow(); void onOpen() override; void onFrame(float duration) override; void updateControlsBox(); void updateLightSettings(); void updateVSyncModeSettings(); void updateWindowModeSettings(); void onResChange(int, int) override; protected: 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(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/sortfilteritemmodel.cpp000066400000000000000000000343661503074453300246350ustar00rootroot00000000000000#include "sortfilteritemmodel.hpp" #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/esmstore.hpp" #include "../mwmechanics/alchemy.hpp" #include "../mwmechanics/spellutil.hpp" namespace { unsigned int getTypeOrder(unsigned int type) { 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) { if (mSortByType && left.mType != right.mType) return left.mType < right.mType; float result = 0; // compare items by type auto leftType = left.mBase.getType(); auto rightType = right.mBase.getType(); if (leftType != rightType) return compareType(leftType, rightType); // compare items by name 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) return result < 0; // compare items by enchantment: // 1. enchanted items showed before non-enchanted // 2. item with lesser charge percent comes after items with more charge percent // 3. item with constant effect comes before items with non-constant effects int leftChargePercent = -1; int rightChargePercent = -1; const ESM::RefId& leftNameEnch = left.mBase.getClass().getEnchantment(left.mBase); const ESM::RefId& rightNameEnch = right.mBase.getClass().getEnchantment(right.mBase); if (!leftNameEnch.empty()) { 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) * 100); } } if (!rightNameEnch.empty()) { 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) * 100); } } result = leftChargePercent - rightChargePercent; if (result != 0) return result > 0; // 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); 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); if (result != 0) return result > 0; // compare items by value result = left.mBase.getClass().getValue(left.mBase) - right.mBase.getClass().getValue(right.mBase); if (result != 0) return result > 0; // compare items by weight result = left.mBase.getClass().getWeight(left.mBase) - right.mBase.getClass().getWeight(right.mBase); if (result != 0) return result > 0; return left.mBase.getCellRef().getRefId() < right.mBase.getCellRef().getRefId(); } }; } namespace MWGui { SortFilterItemModel::SortFilterItemModel(std::unique_ptr sourceModel) : mCategory(Category_All) , mFilter(0) , mSortByType(true) , mApparatusTypeFilter(-1) { mSourceModel = std::move(sourceModel); } bool SortFilterItemModel::allowedToUseItems() const { return mSourceModel->allowedToUseItems(); } void SortFilterItemModel::addDragItem(const MWWorld::Ptr& dragItem, size_t count) { mDragItems.emplace_back(dragItem, count); } void SortFilterItemModel::clearDragItems() { mDragItems.clear(); } bool SortFilterItemModel::filterAccepts(const ItemStack& item) { MWWorld::Ptr base = item.mBase; int category = 0; 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; if (!(category & mCategory)) return false; if (mFilter & Filter_OnlyIngredients) { if (base.getType() != ESM::Ingredient::sRecordId) return false; if (!mNameFilter.empty() && !mEffectFilter.empty()) throw std::logic_error("name and magic effect filter are mutually exclusive"); if (!mNameFilter.empty()) { 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(); 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 = Utf8Stream::lowerCaseUtf8(effect); if (ciEffect.find(mEffectFilter) != std::string::npos) return true; } return false; } return true; } if ((mFilter & Filter_OnlyEnchanted) && !(item.mFlags & ItemStack::Flag_Enchanted)) return false; 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.getType() != ESM::Repair::sRecordId)) return false; 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.getType() == ESM::Book::sRecordId && !base.get()->mBase->mData.mIsScroll) return false; if ((mFilter & Filter_OnlyUsableItems) && base.getClass().getScript(base).empty()) { 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.getType() != ESM::Weapon::sRecordId && base.getType() != ESM::Armor::sRecordId))) return false; if (mFilter & Filter_OnlyRechargable) { if (!(item.mFlags & ItemStack::Flag_Enchanted)) return false; 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(); return false; } 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 = Utf8Stream::lowerCaseUtf8(item.mBase.getClass().getName(item.mBase)); if (compare.find(mNameFilter) == std::string::npos) return false; return true; } ItemStack SortFilterItemModel::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 SortFilterItemModel::getItemCount() { return mItems.size(); } void SortFilterItemModel::setCategory(int category) { mCategory = category; } void SortFilterItemModel::setFilter(int filter) { mFilter = filter; } void SortFilterItemModel::setNameFilter(const std::string& filter) { mNameFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setEffectFilter(const std::string& filter) { mEffectFilter = Utf8Stream::lowerCaseUtf8(filter); } void SortFilterItemModel::setApparatusTypeFilter(const int32_t type) { mApparatusTypeFilter = type; } void SortFilterItemModel::update() { mSourceModel->update(); size_t count = mSourceModel->getItemCount(); mItems.clear(); for (size_t i = 0; i < count; ++i) { ItemStack item = mSourceModel->getItem(i); for (std::vector>::iterator it = mDragItems.begin(); it != mDragItems.end(); ++it) { if (item.mBase == it->first) { if (item.mCount < it->second) throw std::runtime_error("Dragging more than present in the model"); item.mCount -= it->second; } } if (item.mCount > 0 && filterAccepts(item)) mItems.push_back(item); } Compare cmp; cmp.mSortByType = mSortByType; std::sort(mItems.begin(), mItems.end(), cmp); } void SortFilterItemModel::onClose() { mSourceModel->onClose(); } bool SortFilterItemModel::onDropItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onDropItem(item, count); } bool SortFilterItemModel::onTakeItem(const MWWorld::Ptr& item, int count) { return mSourceModel->onTakeItem(item, count); } } openmw-openmw-0.49.0/apps/openmw/mwgui/sortfilteritemmodel.hpp000066400000000000000000000046241503074453300246340ustar00rootroot00000000000000#ifndef MWGUI_SORT_FILTER_ITEM_MODEL_H #define MWGUI_SORT_FILTER_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class SortFilterItemModel : public ProxyItemModel { public: SortFilterItemModel(std::unique_ptr sourceModel); void update() override; bool filterAccepts(const ItemStack& item); bool allowedToUseItems() const 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 clearDragItems(); 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; 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_OnlyAlchemyTools = (1 << 8); private: std::vector mItems; 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 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/soulgemdialog.cpp000066400000000000000000000016241503074453300233620ustar00rootroot00000000000000#include "soulgemdialog.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "messagebox.hpp" namespace MWGui { void SoulgemDialog::show(const MWWorld::Ptr& soulgem) { mSoulgem = soulgem; std::vector buttons; buttons.emplace_back("#{sRechargeEnchantment}"); buttons.emplace_back("#{sMake Enchantment}"); mManager->createInteractiveMessageBox("#{sDoYouWantTo}", buttons); mManager->eventButtonPressed += MyGUI::newDelegate(this, &SoulgemDialog::onButtonPressed); } void SoulgemDialog::onButtonPressed(int button) { if (button == 0) { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Recharge, mSoulgem); } else { MWBase::Environment::get().getWindowManager()->pushGuiMode(GM_Enchanting, mSoulgem); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/soulgemdialog.hpp000066400000000000000000000007531503074453300233710ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_SOULGEMDIALOG_H #define OPENMW_MWGUI_SOULGEMDIALOG_H #include "../mwworld/ptr.hpp" namespace MWGui { class MessageBoxManager; class SoulgemDialog { public: SoulgemDialog(MessageBoxManager* manager) : mManager(manager) { } void show(const MWWorld::Ptr& soulgem); void onButtonPressed(int button); private: MessageBoxManager* mManager; MWWorld::Ptr mSoulgem; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellbuyingwindow.cpp000066400000000000000000000170251503074453300243160ustar00rootroot00000000000000#include "spellbuyingwindow.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spells.hpp" namespace MWGui { SpellBuyingWindow::SpellBuyingWindow() : WindowBase("openmw_spell_buying_window.layout") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mSpellsView, "SpellsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellBuyingWindow::onCancelButtonClicked); } bool SpellBuyingWindow::sortSpells(const ESM::Spell* left, const ESM::Spell* right) { return Misc::StringUtils::ciLess(left->mName, right->mName); } void SpellBuyingWindow::addSpell(const ESM::Spell& spell) { 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); MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); // TODO: refactor to use MyGUI::ListBox 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); mCurrentY += lineHeight; toAdd->setUserData(price); 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.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)); } void SpellBuyingWindow::clearSpells() { 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) { 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(); std::vector spellsToSort; for (const ESM::Spell* spell : merchantSpells) { 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().getESMStore()->get().find( actor.get()->mBase->mRace); if (race->mPowers.exists(spell->mId)) continue; } if (playerHasSpell(spell->mId)) continue; spellsToSort.push_back(spell); } std::stable_sort(spellsToSort.begin(), spellsToSort.end(), sortSpells); for (const ESM::Spell* spell : spellsToSort) { addSpell(*spell); } spellsToSort.clear(); updateLabels(); // 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->setVisibleVScroll(true); mSpellsView->setViewOffset(MyGUI::IntPoint(0, startOffset)); } bool SpellBuyingWindow::playerHasSpell(const ESM::RefId& id) { MWWorld::Ptr player = MWMechanics::getPlayer(); return player.getClass().getCreatureStats(player).getSpells().hasSpell(id); } void SpellBuyingWindow::onSpellButtonClick(MyGUI::Widget* _sender) { int price = *_sender->getUserData(); MWWorld::Ptr player = MWMechanics::getPlayer(); if (price > player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId)) return; MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); 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); npcStats.setGoldPool(npcStats.getGoldPool() + price); setPtr(mPtr, mSpellsView->getViewOffset().top); 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); } void SpellBuyingWindow::updateLabels() { MWWorld::Ptr player = MWMechanics::getPlayer(); 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()); } void SpellBuyingWindow::onReferenceUnavailable() { // 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) mSpellsView->setViewOffset(MyGUI::IntPoint(0, 0)); else mSpellsView->setViewOffset( MyGUI::IntPoint(0, static_cast(mSpellsView->getViewOffset().top + _rel * 0.3f))); } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellbuyingwindow.hpp000066400000000000000000000027421503074453300243230ustar00rootroot00000000000000#ifndef MWGUI_SpellBuyingWINDOW_H #define MWGUI_SpellBuyingWINDOW_H #include "referenceinterface.hpp" #include "windowbase.hpp" #include namespace ESM { struct Spell; } namespace MyGUI { class Gui; class Widget; } namespace MWGui { class SpellBuyingWindow : public ReferenceInterface, public WindowBase { public: SpellBuyingWindow(); 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 onResChange(int, int) override { center(); } std::string_view getWindowIdForLua() const override { return "SpellBuying"; } protected: MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; MyGUI::ScrollView* mSpellsView; 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 updateLabels(); void onReferenceUnavailable() override; bool playerHasSpell(const ESM::RefId& id); private: static bool sortSpells(const ESM::Spell* left, const ESM::Spell* right); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellcreationdialog.cpp000066400000000000000000000670601503074453300245610ustar00rootroot00000000000000#include "spellcreationdialog.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/store.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellutil.hpp" #include "class.hpp" #include "tooltips.hpp" #include "widgets.hpp" namespace { bool sortMagicEffects(short id1, short id2) { const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); return gmst.find(ESM::MagicEffect::indexToGmstString(id1))->mValue.getString() < gmst.find(ESM::MagicEffect::indexToGmstString(id2))->mValue.getString(); } void init(ESM::ENAMstruct& effect) { effect.mArea = 0; effect.mDuration = 0; effect.mEffectID = -1; effect.mMagnMax = 0; effect.mMagnMin = 0; effect.mRange = 0; effect.mSkill = -1; effect.mAttribute = -1; } } namespace MWGui { EditEffectDialog::EditEffectDialog() : WindowModal("openmw_edit_effect.layout") , mEditing(false) , mMagicEffect(nullptr) , mConstantEffect(false) { init(mEffect); init(mOldEffect); getWidget(mCancelButton, "CancelButton"); getWidget(mOkButton, "OkButton"); getWidget(mDeleteButton, "DeleteButton"); getWidget(mRangeButton, "RangeButton"); getWidget(mMagnitudeMinValue, "MagnitudeMinValue"); getWidget(mMagnitudeMaxValue, "MagnitudeMaxValue"); getWidget(mDurationValue, "DurationValue"); getWidget(mAreaValue, "AreaValue"); getWidget(mMagnitudeMinSlider, "MagnitudeMinSlider"); getWidget(mMagnitudeMaxSlider, "MagnitudeMaxSlider"); getWidget(mDurationSlider, "DurationSlider"); getWidget(mAreaSlider, "AreaSlider"); getWidget(mEffectImage, "EffectImage"); getWidget(mEffectName, "EffectName"); getWidget(mAreaText, "AreaText"); getWidget(mDurationBox, "DurationBox"); getWidget(mAreaBox, "AreaBox"); getWidget(mMagnitudeBox, "MagnitudeBox"); mRangeButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onRangeButtonClicked); mOkButton->eventMouseButtonClick += MyGUI::newDelegate(this, &EditEffectDialog::onOkButtonClicked); 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); mDurationSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onDurationChanged); mAreaSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &EditEffectDialog::onAreaChanged); } void EditEffectDialog::setConstantEffect(bool constant) { mConstantEffect = constant; } void EditEffectDialog::onOpen() { WindowModal::onOpen(); center(); } bool EditEffectDialog::exit() { if (mEditing) eventEffectModified(mOldEffect); else eventEffectRemoved(mEffect); return true; } void EditEffectDialog::newEffect(const ESM::MagicEffect* effect) { bool allowSelf = (effect->mData.mFlags & ESM::MagicEffect::CastSelf) != 0 || mConstantEffect; bool allowTouch = (effect->mData.mFlags & ESM::MagicEffect::CastTouch) && !mConstantEffect; setMagicEffect(effect); mEditing = false; mDeleteButton->setVisible(false); mEffect.mRange = ESM::RT_Self; if (!allowSelf) mEffect.mRange = ESM::RT_Touch; if (!allowTouch) mEffect.mRange = ESM::RT_Target; mEffect.mMagnMin = 1; mEffect.mMagnMax = 1; mEffect.mDuration = 1; mEffect.mArea = 0; mEffect.mSkill = -1; mEffect.mAttribute = -1; eventEffectAdded(mEffect); onRangeButtonClicked(mRangeButton); 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", "-") }; mMagnitudeMaxValue->setCaption(to + " 1"); mAreaValue->setCaption("0"); setVisible(true); } void EditEffectDialog::editEffect(ESM::ENAMstruct effect) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); setMagicEffect(magicEffect); mOldEffect = effect; mEffect = effect; mEditing = true; mDeleteButton->setVisible(true); 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}"); else if (mEffect.mRange == ESM::RT_Target) mRangeButton->setCaptionWithReplacing("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) mRangeButton->setCaptionWithReplacing("#{sRangeTouch}"); 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) { mEffectImage->setImageTexture(Misc::ResourceHelpers::correctIconPath( effect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); mEffectName->setCaptionWithReplacing("#{" + ESM::MagicEffect::indexToGmstString(effect->mIndex) + "}"); mEffect.mEffectID = effect->mIndex; mMagicEffect = effect; updateBoxes(); } void EditEffectDialog::updateBoxes() { static int startY = mMagnitudeBox->getPosition().top; int curY = startY; 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); curY += mMagnitudeBox->getSize().height; } if (!(mMagicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) && mConstantEffect == false) { mDurationBox->setPosition(mDurationBox->getPosition().left, curY); 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; } } void EditEffectDialog::onRangeButtonClicked(MyGUI::Widget* sender) { 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 || 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; if (mEffect.mRange == ESM::RT_Touch && !allowTouch) mEffect.mRange = (mEffect.mRange + 1) % 3; if (mEffect.mRange == ESM::RT_Target && !allowTarget) mEffect.mRange = (mEffect.mRange + 1) % 3; if (mEffect.mRange == ESM::RT_Self) { mAreaSlider->setScrollPosition(0); onAreaChanged(mAreaSlider, 0); } if (mEffect.mRange == ESM::RT_Self) mRangeButton->setCaptionWithReplacing("#{sRangeSelf}"); else if (mEffect.mRange == ESM::RT_Target) mRangeButton->setCaptionWithReplacing("#{sRangeTarget}"); else if (mEffect.mRange == ESM::RT_Touch) mRangeButton->setCaptionWithReplacing("#{sRangeTouch}"); updateBoxes(); eventEffectModified(mEffect); } void EditEffectDialog::onDeleteButtonClicked(MyGUI::Widget* sender) { setVisible(false); eventEffectRemoved(mEffect); } void EditEffectDialog::onOkButtonClicked(MyGUI::Widget* sender) { setVisible(false); } void EditEffectDialog::onCancelButtonClicked(MyGUI::Widget* sender) { setVisible(false); exit(); } void EditEffectDialog::setSkill(ESM::RefId skill) { mEffect.mSkill = ESM::Skill::refIdToIndex(skill); eventEffectModified(mEffect); } void EditEffectDialog::setAttribute(ESM::RefId attribute) { mEffect.mAttribute = ESM::Attribute::refIdToIndex(attribute); eventEffectModified(mEffect); } void EditEffectDialog::onMagnitudeMinChanged(MyGUI::ScrollBar* sender, size_t pos) { mMagnitudeMinValue->setCaption(MyGUI::utility::toString(pos + 1)); mEffect.mMagnMin = pos + 1; // trigger the check again (see below) onMagnitudeMaxChanged(mMagnitudeMaxSlider, mMagnitudeMaxSlider->getScrollPosition()); eventEffectModified(mEffect); } 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) { pos = mEffect.mMagnMin - 1; sender->setScrollPosition(pos); } mEffect.mMagnMax = pos + 1; const std::string to{ MWBase::Environment::get().getWindowManager()->getGameSettingString("sTo", "-") }; mMagnitudeMaxValue->setCaption(to + " " + MyGUI::utility::toString(pos + 1)); eventEffectModified(mEffect); } void EditEffectDialog::onDurationChanged(MyGUI::ScrollBar* sender, size_t pos) { mDurationValue->setCaption(MyGUI::utility::toString(pos + 1)); mEffect.mDuration = pos + 1; eventEffectModified(mEffect); } void EditEffectDialog::onAreaChanged(MyGUI::ScrollBar* sender, size_t pos) { mAreaValue->setCaption(MyGUI::utility::toString(pos)); mEffect.mArea = pos; eventEffectModified(mEffect); } // ------------------------------------------------------------------------------------------------ SpellCreationDialog::SpellCreationDialog() : WindowBase("openmw_spellcreation_dialog.layout") , EffectEditorBase(EffectEditorBase::Spellmaking) { getWidget(mNameEdit, "NameEdit"); getWidget(mMagickaCost, "MagickaCost"); getWidget(mSuccessChance, "SuccessChance"); getWidget(mAvailableEffectsList, "AvailableEffects"); getWidget(mUsedEffectsView, "UsedEffects"); getWidget(mPriceLabel, "PriceLabel"); getWidget(mBuyButton, "BuyButton"); getWidget(mCancelButton, "CancelButton"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onCancelButtonClicked); mBuyButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellCreationDialog::onBuyButtonClicked); mNameEdit->eventEditSelectAccept += MyGUI::newDelegate(this, &SpellCreationDialog::onAccept); setWidgets(mAvailableEffectsList, mUsedEffectsView); } 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({}); startEditing(); } void SpellCreationDialog::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_SpellCreation); } void SpellCreationDialog::onBuyButtonClicked(MyGUI::Widget* sender) { if (mEffects.size() <= 0) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage30}"); return; } if (mNameEdit->getCaption().empty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage10}"); return; } if (mMagickaCost->getCaption() == "0") { MWBase::Environment::get().getWindowManager()->messageBox("#{sEnchantmentMenu8}"); return; } MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); int price = MyGUI::utility::parseInt(mPriceLabel->getCaption()); if (price > playerGold) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage18}"); return; } mSpell.mName = mNameEdit->getCaption(); 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(ESM::RefId::stringRefId("Mysticism Hit")); 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); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellCreation); } void SpellCreationDialog::onAccept(MyGUI::EditBox* sender) { onBuyButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void SpellCreationDialog::onOpen() { center(); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mNameEdit); } void SpellCreationDialog::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Dialogue); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_SpellCreation); } void SpellCreationDialog::notifyEffectsChanged() { if (mEffects.empty()) { mMagickaCost->setCaption("0"); mPriceLabel->setCaption("0"); mSuccessChance->setCaption("0"); return; } float y = 0; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); for (const ESM::ENAMstruct& effect : mEffects) { y += std::max( 1.f, MWMechanics::calcEffectCost(effect, nullptr, MWMechanics::EffectCostMethod::PlayerSpell)); if (effect.mRange == ESM::RT_Target) y *= 1.5; } 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(); int price = std::max(1, static_cast(y * fSpellMakingValueMult)); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); mPriceLabel->setCaption(MyGUI::utility::toString(int(price))); float chance = MWMechanics::calcSpellBaseSuccessChance(&mSpell, MWMechanics::getPlayer(), nullptr); int intChance = std::min(100, int(chance)); mSuccessChance->setCaption(MyGUI::utility::toString(intChance)); } // ------------------------------------------------------------------------------------------------ EffectEditorBase::EffectEditorBase(Type type) : mAvailableEffectsList(nullptr) , mUsedEffectsView(nullptr) , mAddEffectDialog() , mSelectedEffect(0) , mSelectedKnownEffectId(0) , mConstantEffect(false) , mType(type) { mAddEffectDialog.eventEffectAdded += MyGUI::newDelegate(this, &EffectEditorBase::onEffectAdded); mAddEffectDialog.eventEffectModified += MyGUI::newDelegate(this, &EffectEditorBase::onEffectModified); mAddEffectDialog.eventEffectRemoved += MyGUI::newDelegate(this, &EffectEditorBase::onEffectRemoved); mAddEffectDialog.setVisible(false); } EffectEditorBase::~EffectEditorBase() {} void EffectEditorBase::startEditing() { // get the list of magic effects that are known to the player MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); std::vector knownEffects; for (const ESM::Spell* spell : spells) { // only normal spells count if (spell->mData.mType != ESM::Spell::ST_Spell) continue; for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList) { 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; if (!(effect->mData.mFlags & requiredFlags)) continue; if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end()) knownEffects.push_back(effectId); } } std::sort(knownEffects.begin(), knownEffects.end(), sortMagicEffects); mAvailableEffectsList->clear(); int i = 0; for (const short effectId : knownEffects) { mAvailableEffectsList->addItem(MWBase::Environment::get() .getESMStore() ->get() .find(ESM::MagicEffect::indexToGmstString(effectId)) ->mValue.getString()); mButtonMapping[i] = effectId; ++i; } mAvailableEffectsList->adjustSize(); mAvailableEffectsList->scrollToTop(); for (const short effectId : knownEffects) { 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); } mEffects.clear(); updateEffectsView(); } void EffectEditorBase::setWidgets(Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView) { mAvailableEffectsList = availableEffectsList; mUsedEffectsView = usedEffectsView; mAvailableEffectsList->eventWidgetSelected += MyGUI::newDelegate(this, &EffectEditorBase::onAvailableEffectClicked); } void EffectEditorBase::onSelectAttribute() { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); mAddEffectDialog.setAttribute(mSelectAttributeDialog->getAttributeId()); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectAttributeDialog)); } void EffectEditorBase::onSelectSkill() { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find(mSelectedKnownEffectId); mAddEffectDialog.newEffect(effect); mAddEffectDialog.setSkill(mSelectSkillDialog->getSkillId()); MWBase::Environment::get().getWindowManager()->removeDialog(std::move(mSelectSkillDialog)); } void EffectEditorBase::onAttributeOrSkillCancel() { 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) { if (mEffects.size() >= 8) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage28}"); return; } int buttonId = *sender->getUserData(); mSelectedKnownEffectId = mButtonMapping[buttonId]; 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) { mSelectSkillDialog = std::make_unique(); mSelectSkillDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); mSelectSkillDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectSkill); mSelectSkillDialog->setVisible(true); } else if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) { mSelectAttributeDialog = std::make_unique(); mSelectAttributeDialog->eventCancel += MyGUI::newDelegate(this, &SpellCreationDialog::onAttributeOrSkillCancel); mSelectAttributeDialog->eventItemSelected += MyGUI::newDelegate(this, &SpellCreationDialog::onSelectAttribute); mSelectAttributeDialog->setVisible(true); } else { for (const ESM::ENAMstruct& effectInfo : mEffects) { if (effectInfo.mEffectID == mSelectedKnownEffectId) { MWBase::Environment::get().getWindowManager()->messageBox("#{sOnetypeEffectMessage}"); return; } } mAddEffectDialog.newEffect(effect); } } void EffectEditorBase::onEffectModified(ESM::ENAMstruct effect) { mEffects[mSelectedEffect] = effect; updateEffectsView(); } void EffectEditorBase::onEffectRemoved(ESM::ENAMstruct effect) { mEffects.erase(mEffects.begin() + mSelectedEffect); updateEffectsView(); } void EffectEditorBase::updateEffectsView() { MyGUI::EnumeratorWidgetPtr oldWidgets = mUsedEffectsView->getEnumerator(); MyGUI::Gui::getInstance().destroyWidgets(oldWidgets); MyGUI::IntSize size(0, 0); int i = 0; for (const ESM::ENAMstruct& effectInfo : mEffects) { Widgets::SpellEffectParams params; params.mEffectID = effectInfo.mEffectID; 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; params.mRange = effectInfo.mRange; params.mArea = effectInfo.mArea; params.mIsConstant = mConstantEffect; 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); Widgets::MWSpellEffectPtr effect = button->createWidget( "MW_EffectImage", MyGUI::IntCoord(0, 0, 0, 24), MyGUI::Align::Default); effect->setNeedMouseFocus(false); effect->setSpellEffect(params); effect->setSize(effect->getRequestedWidth(), 24); button->setSize(effect->getRequestedWidth(), 24); 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 mUsedEffectsView->setVisibleHScroll(false); mUsedEffectsView->setCanvasSize(size); mUsedEffectsView->setVisibleHScroll(true); notifyEffectsChanged(); } void EffectEditorBase::onEffectAdded(ESM::ENAMstruct effect) { mEffects.push_back(effect); mSelectedEffect = mEffects.size() - 1; updateEffectsView(); } void EffectEditorBase::onEditEffect(MyGUI::Widget* sender) { int id = *sender->getUserData(); mSelectedEffect = id; 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; } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellcreationdialog.hpp000066400000000000000000000114621503074453300245610ustar00rootroot00000000000000#ifndef MWGUI_SPELLCREATION_H #define MWGUI_SPELLCREATION_H #include #include #include #include "referenceinterface.hpp" #include "windowbase.hpp" namespace Gui { class MWList; } namespace MWGui { class SelectSkillDialog; class SelectAttributeDialog; class EditEffectDialog : public WindowModal { public: EditEffectDialog(); void onOpen() override; bool exit() override; void setConstantEffect(bool constant); void setSkill(ESM::RefId skill); void setAttribute(ESM::RefId attribute); void newEffect(const ESM::MagicEffect* effect); void editEffect(ESM::ENAMstruct effect); typedef MyGUI::delegates::MultiDelegate EventHandle_Effect; EventHandle_Effect eventEffectAdded; EventHandle_Effect eventEffectModified; EventHandle_Effect eventEffectRemoved; protected: MyGUI::Button* mCancelButton; MyGUI::Button* mOkButton; MyGUI::Button* mDeleteButton; MyGUI::Button* mRangeButton; MyGUI::Widget* mDurationBox; MyGUI::Widget* mMagnitudeBox; MyGUI::Widget* mAreaBox; MyGUI::TextBox* mMagnitudeMinValue; MyGUI::TextBox* mMagnitudeMaxValue; MyGUI::TextBox* mDurationValue; MyGUI::TextBox* mAreaValue; MyGUI::ScrollBar* mMagnitudeMinSlider; MyGUI::ScrollBar* mMagnitudeMaxSlider; MyGUI::ScrollBar* mDurationSlider; MyGUI::ScrollBar* mAreaSlider; MyGUI::TextBox* mAreaText; MyGUI::ImageBox* mEffectImage; MyGUI::TextBox* mEffectName; 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 setMagicEffect(const ESM::MagicEffect* effect); void updateBoxes(); protected: ESM::ENAMstruct mEffect; ESM::ENAMstruct mOldEffect; const ESM::MagicEffect* mMagicEffect; bool mConstantEffect; }; class EffectEditorBase { public: enum Type { Spellmaking, Enchanting }; EffectEditorBase(Type type); virtual ~EffectEditorBase(); void setConstantEffect(bool constant); protected: std::map mButtonMapping; // maps button ID to effect ID Gui::MWList* mAvailableEffectsList; MyGUI::ScrollView* mUsedEffectsView; EditEffectDialog mAddEffectDialog; std::unique_ptr mSelectAttributeDialog; std::unique_ptr mSelectSkillDialog; int mSelectedEffect; short mSelectedKnownEffectId; bool mConstantEffect; std::vector mEffects; void onEffectAdded(ESM::ENAMstruct effect); void onEffectModified(ESM::ENAMstruct effect); void onEffectRemoved(ESM::ENAMstruct effect); void onAvailableEffectClicked(MyGUI::Widget* sender); void onAttributeOrSkillCancel(); void onSelectAttribute(); void onSelectSkill(); void onEditEffect(MyGUI::Widget* sender); void updateEffectsView(); void startEditing(); void setWidgets(Gui::MWList* availableEffectsList, MyGUI::ScrollView* usedEffectsView); virtual void notifyEffectsChanged() {} private: Type mType; }; class SpellCreationDialog : public WindowBase, public ReferenceInterface, public EffectEditorBase { public: SpellCreationDialog(); void onOpen() override; void clear() override { resetReference(); } void onFrame(float dt) override { checkReferenceAvailable(); } 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 onAccept(MyGUI::EditBox* sender); void notifyEffectsChanged() override; MyGUI::EditBox* mNameEdit; MyGUI::TextBox* mMagickaCost; MyGUI::TextBox* mSuccessChance; MyGUI::Button* mBuyButton; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPriceLabel; ESM::Spell mSpell; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellicons.cpp000066400000000000000000000210451503074453300227010ustar00rootroot00000000000000#include "spellicons.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "tooltips.hpp" namespace MWGui { void SpellIcons::updateWidgets(MyGUI::Widget* parent, bool adjustSize) { 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); } } int w = 2; const auto& store = MWBase::Environment::get().getESMStore(); for (const auto& [effectId, effectInfos] : effects) { const ESM::MagicEffect* effect = store->get().find(effectId); float remainingDuration = 0; float totalDuration = 0; std::string sourcesDescription; static const float fadeTime = store->get().find("fMagicStartIconBlink")->mValue.getFloat(); bool addNewLine = false; for (const MagicEffectInfo& effectInfo : effectInfos) { if (addNewLine) sourcesDescription += '\n'; // if at least one of the effect sources is permanent, the effect will never wear off if (effectInfo.mPermanent) { remainingDuration = fadeTime; totalDuration = fadeTime; } else { remainingDuration = std::max(remainingDuration, effectInfo.mRemainingTime); totalDuration = std::max(totalDuration, effectInfo.mTotalTime); } sourcesDescription += effectInfo.mSource; if (effect->mData.mFlags & ESM::MagicEffect::TargetSkill) { const ESM::Skill* skill = store->get().find(effectInfo.mKey.mArg); sourcesDescription += " (" + skill->mName + ')'; } if (effect->mData.mFlags & ESM::MagicEffect::TargetAttribute) { 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_view timesInt = MWBase::Environment::get().getWindowManager()->getGameSettingString("sXTimesINT", {}); std::stringstream formatter; formatter << std::fixed << std::setprecision(1) << " " << (effectInfo.mMagnitude / 10.0f) << timesInt; sourcesDescription += formatter.str(); } 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 += ' '; sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("sfeet", {}); } else if (displayType == ESM::MagicEffect::MDT_Level) { 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 += ' '; if (effectInfo.mMagnitude > 1) sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("spoints", {}); else sourcesDescription += MWBase::Environment::get().getWindowManager()->getGameSettingString("spoint", {}); } } if (effectInfo.mRemainingTime > -1 && Settings::game().mShowEffectDuration) sourcesDescription += MWGui::ToolTips::getDurationString(effectInfo.mRemainingTime, " #{sDuration}"); addNewLine = true; } if (remainingDuration > 0.f) { MyGUI::ImageBox* image; if (mWidgetMap.find(effectId) == mWidgetMap.end()) { image = parent->createWidget( "ImageBox", MyGUI::IntCoord(w, 2, 16, 16), MyGUI::Align::Default); mWidgetMap[effectId] = image; image->setImageTexture(Misc::ResourceHelpers::correctIconPath( effect->mIcon, MWBase::Environment::get().getResourceSystem()->getVFS())); const std::string& name = ESM::MagicEffect::indexToGmstString(effectId); ToolTipInfo tooltipInfo; tooltipInfo.caption = "#{" + name + "}"; tooltipInfo.icon = effect->mIcon; tooltipInfo.imageSize = 16; tooltipInfo.wordWrap = false; image->setUserData(tooltipInfo); image->setUserString("ToolTipType", "ToolTipInfo"); } else image = mWidgetMap[effectId]; image->setPosition(w, 2); image->setVisible(true); w += 16; ToolTipInfo* tooltipInfo = image->getUserData(); tooltipInfo->text = std::move(sourcesDescription); // Fade out if (totalDuration >= fadeTime && fadeTime > 0.f) image->setAlpha(std::min(remainingDuration / fadeTime, 1.f)); } else if (mWidgetMap.find(effectId) != mWidgetMap.end()) { MyGUI::ImageBox* image = mWidgetMap[effectId]; image->setVisible(false); image->setAlpha(1.f); } } if (adjustSize) { int s = w + 2; if (effects.empty()) s = 0; int diff = parent->getWidth() - s; parent->setSize(s, parent->getHeight()); parent->setPosition(parent->getLeft() + diff, parent->getTop()); } // hide inactive effects for (auto& widgetPair : mWidgetMap) { if (effects.find(widgetPair.first) == effects.end()) widgetPair.second->setVisible(false); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellicons.hpp000066400000000000000000000020001503074453300226740ustar00rootroot00000000000000#ifndef MWGUI_SPELLICONS_H #define MWGUI_SPELLICONS_H #include #include #include "../mwmechanics/magiceffects.hpp" namespace MyGUI { class Widget; class ImageBox; } namespace ESM { struct ENAMstruct; struct EffectList; } namespace MWGui { // information about a single magic effect source as required for display in the tooltip struct MagicEffectInfo { MagicEffectInfo() : mMagnitude(0) , 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; float mRemainingTime; float mTotalTime; bool mPermanent; // the effect is permanent }; class SpellIcons { public: void updateWidgets(MyGUI::Widget* parent, bool adjustSize); private: std::map mWidgetMap; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellmodel.cpp000066400000000000000000000154371503074453300226760ustar00rootroot00000000000000#include "spellmodel.hpp" #include #include #include #include #include "../mwbase/environment.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" namespace { bool sortSpells(const MWGui::Spell& left, const MWGui::Spell& right) { if (left.mType != right.mType) return left.mType < right.mType; return Misc::StringUtils::ciLess(left.mName, right.mName); } } namespace MWGui { SpellModel::SpellModel(const MWWorld::Ptr& actor, const std::string& filter) : mActor(actor) , mFilter(filter) { } SpellModel::SpellModel(const MWWorld::Ptr& actor) : mActor(actor) { } bool SpellModel::matchingEffectExists(std::string filter, const ESM::EffectList& effects) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); for (const auto& effect : effects.mList) { short effectId = effect.mData.mEffectID; if (effectId != -1) { 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)); std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName); if (convert.find(filter) != std::string::npos) { return true; } } } return false; } void SpellModel::update() { mSpells.clear(); MWMechanics::CreatureStats& stats = mActor.getClass().getCreatureStats(mActor); const MWMechanics::Spells& spells = stats.getSpells(); const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); std::string filter = Utf8Stream::lowerCaseUtf8(mFilter); for (const ESM::Spell* spell : spells) { if (spell->mData.mType != ESM::Spell::ST_Power && spell->mData.mType != ESM::Spell::ST_Spell) continue; std::string name = Utf8Stream::lowerCaseUtf8(spell->mName); if (name.find(filter) == std::string::npos && !matchingEffectExists(filter, spell->mEffects)) continue; Spell newSpell; newSpell.mName = spell->mName; if (spell->mData.mType == ESM::Spell::ST_Spell) { newSpell.mType = Spell::Type_Spell; std::string cost = std::to_string(MWMechanics::calcSpellCost(*spell)); std::string chance = std::to_string(int(MWMechanics::getSpellSuccessChance(spell, mActor))); newSpell.mCostColumn = cost + "/" + chance; } else newSpell.mType = Spell::Type_Power; newSpell.mId = spell->mId; newSpell.mSelected = (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == spell->mId); newSpell.mActive = true; newSpell.mCount = 1; mSpells.push_back(newSpell); } MWWorld::InventoryStore& invStore = mActor.getClass().getInventoryStore(mActor); for (MWWorld::ContainerStoreIterator it = invStore.begin(); it != invStore.end(); ++it) { MWWorld::Ptr item = *it; 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(); continue; } if (enchant->mData.mType != ESM::Enchantment::WhenUsed && enchant->mData.mType != ESM::Enchantment::CastOnce) continue; std::string name = Utf8Stream::lowerCaseUtf8(item.getClass().getName(item)); 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.getCellRef().getCount(); newSpell.mType = Spell::Type_EnchantedItem; newSpell.mSelected = invStore.getSelectedEnchantItem() == it; // FIXME: move to mwmechanics if (enchant->mData.mType == ESM::Enchantment::CastOnce) { newSpell.mCostColumn = "100/100"; newSpell.mActive = false; } else { if (!item.getClass().getEquipmentSlots(item).first.empty() && item.getClass().canBeEquipped(item, mActor).first == 0) continue; int castCost = MWMechanics::getEffectiveEnchantmentCastCost(*enchant, mActor); std::string cost = std::to_string(castCost); int currentCharge = int(item.getCellRef().getEnchantmentCharge()); if (currentCharge == -1) currentCharge = MWMechanics::getEnchantmentCharge(*enchant); std::string charge = std::to_string(currentCharge); newSpell.mCostColumn = cost + "/" + charge; newSpell.mActive = invStore.isEquipped(item); } mSpells.push_back(newSpell); } std::stable_sort(mSpells.begin(), mSpells.end(), sortSpells); } size_t SpellModel::getItemCount() const { return mSpells.size(); } SpellModel::ModelIndex SpellModel::getSelectedIndex() const { ModelIndex selected = -1; for (SpellModel::ModelIndex i = 0; i < int(getItemCount()); ++i) { if (getItem(i).mSelected) { selected = i; break; } } return selected; } Spell SpellModel::getItem(ModelIndex index) const { if (index < 0 || index >= int(mSpells.size())) throw std::runtime_error("invalid spell index supplied"); return mSpells[index]; } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellmodel.hpp000066400000000000000000000031411503074453300226700ustar00rootroot00000000000000#ifndef OPENMW_GUI_SPELLMODEL_H #define OPENMW_GUI_SPELLMODEL_H #include "../mwworld/ptr.hpp" #include namespace MWGui { struct Spell { enum Type { Type_Power, Type_Spell, Type_EnchantedItem }; Type mType; std::string mName; std::string mCostColumn; // Cost/chance or Cost/charge 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) bool mActive; // (Items only) is the item equipped? Spell() : mType(Type_Spell) , mCount(0) , mSelected(false) , mActive(false) { } }; ///@brief Model that lists all usable powers, spells and enchanted items for an actor. class SpellModel { public: SpellModel(const MWWorld::Ptr& actor, const std::string& filter); SpellModel(const MWWorld::Ptr& actor); typedef int ModelIndex; void update(); Spell getItem(ModelIndex index) const; ///< throws for invalid index size_t getItemCount() const; ModelIndex getSelectedIndex() const; ///< returns -1 if nothing is selected private: MWWorld::Ptr mActor; std::vector mSpells; std::string mFilter; bool matchingEffectExists(std::string filter, const ESM::EffectList& effects); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellview.cpp000066400000000000000000000254761503074453300225540ustar00rootroot00000000000000#include "spellview.hpp" #include #include #include #include #include #include #include #include "tooltips.hpp" namespace MWGui { const char* SpellView::sSpellModelIndex = "SpellModelIndex"; SpellView::LineInfo::LineInfo( MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex) : mLeftWidget(leftWidget) , mRightWidget(rightWidget) , mSpellIndex(spellIndex) { } SpellView::SpellView() : mScrollView(nullptr) , mShowCostColumn(true) , mHighlightSelected(true) { } void SpellView::initialiseOverride() { Base::initialiseOverride(); assignWidget(mScrollView, "ScrollView"); if (mScrollView == nullptr) throw std::runtime_error("Item view needs a scroll view"); mScrollView->setCanvasAlign(MyGUI::Align::Left | MyGUI::Align::Top); } void SpellView::registerComponents() { MyGUI::FactoryManager::getInstance().registerFactory("Widget"); } void SpellView::setModel(SpellModel* model) { mModel.reset(model); update(); } SpellModel* SpellView::getModel() { return mModel.get(); } void SpellView::setShowCostColumn(bool show) { if (show != mShowCostColumn) { mShowCostColumn = show; update(); } } void SpellView::setHighlightSelected(bool highlight) { if (highlight != mHighlightSelected) { mHighlightSelected = highlight; update(); } } void SpellView::update() { if (!mModel.get()) return; mModel->update(); int curType = -1; const int spellHeight = Settings::gui().mFontSize + 2; mLines.clear(); while (mScrollView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mScrollView->getChildAt(0)); 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}", {}); else if (spell.mType == Spell::Type_Spell) addGroup("#{sSpells}", mShowCostColumn ? "#{sCostChance}" : ""); else addGroup("#{sMagicItem}", mShowCostColumn ? "#{sCostCharge}" : ""); curType = spell.mType; } 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); t->setNeedKeyFocus(true); t->setCaption(spell.mName + captionSuffix); t->setTextAlign(MyGUI::Align::Left); adjustSpellWidget(spell, i, t); if (!spell.mCostColumn.empty() && mShowCostColumn) { 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); Gui::ButtonGroup group; group.push_back(t); group.push_back(costChance); Gui::SharedStateButton::createButtonGroup(group); mLines.emplace_back(t, costChance, i); } else mLines.emplace_back(t, (MyGUI::Widget*)nullptr, i); t->setStateSelected(spell.mSelected); } layoutWidgets(); } void SpellView::incrementalUpdate() { if (!mModel.get()) { return; } mModel->update(); bool fullUpdateRequired = false; SpellModel::ModelIndex maxSpellIndexFound = -1; for (LineInfo& line : mLines) { // only update the lines that are "updateable" SpellModel::ModelIndex spellIndex(line.mSpellIndex); if (spellIndex != NoSpellIndex) { Gui::SharedStateButton* nameButton = reinterpret_cast(line.mLeftWidget); // match model against line // if don't match, then major change has happened, so do a full update if (mModel->getItemCount() <= static_cast(spellIndex)) { fullUpdateRequired = true; break; } // more checking for major change. const Spell& spell = mModel->getItem(spellIndex); const std::string captionSuffix = MWGui::ToolTips::getCountString(spell.mCount); if (nameButton->getCaption() != (spell.mName + captionSuffix)) { fullUpdateRequired = true; break; } else { maxSpellIndexFound = spellIndex; Gui::SharedStateButton* costButton = reinterpret_cast(line.mRightWidget); if ((costButton != nullptr) && (costButton->getCaption() != spell.mCostColumn)) { costButton->setCaption(spell.mCostColumn); } } } } // 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))) { update(); } } void SpellView::layoutWidgets() { int height = 0; for (LineInfo& line : mLines) { height += line.mLeftWidget->getHeight(); } bool scrollVisible = height > mScrollView->getHeight(); int width = mScrollView->getWidth() - (scrollVisible ? 18 : 0); height = 0; for (LineInfo& line : mLines) { int lineHeight = line.mLeftWidget->getHeight(); line.mLeftWidget->setCoord(4, height, width - 8, lineHeight); if (line.mRightWidget) { line.mRightWidget->setCoord(4, height, width - 8, lineHeight); MyGUI::TextBox* second = line.mRightWidget->castType(false); if (second) line.mLeftWidget->setSize(width - 8 - second->getTextSize().width, lineHeight); } height += lineHeight; } // 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) { if (mScrollView->getChildCount() > 0) { 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); groupWidget->setCaptionWithReplacing(label); groupWidget->setTextAlign(MyGUI::Align::Left); groupWidget->setNeedMouseFocus(false); if (!label2.empty()) { MyGUI::TextBox* groupWidget2 = mScrollView->createWidget("SandBrightText", MyGUI::IntCoord(0, 0, mScrollView->getWidth(), 24), MyGUI::Align::Left | MyGUI::Align::Top); groupWidget2->setCaptionWithReplacing(label2); groupWidget2->setTextAlign(MyGUI::Align::Right); groupWidget2->setNeedMouseFocus(false); mLines.emplace_back(groupWidget, groupWidget2, NoSpellIndex); } else mLines.emplace_back(groupWidget, (MyGUI::Widget*)nullptr, NoSpellIndex); } void SpellView::setSize(const MyGUI::IntSize& _value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setSize(_value); if (changed) layoutWidgets(); } void SpellView::setCoord(const MyGUI::IntCoord& _value) { bool changed = (_value.width != getWidth() || _value.height != getHeight()); Base::setCoord(_value); if (changed) layoutWidgets(); } void SpellView::adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget) { if (spell.mType == Spell::Type_EnchantedItem) { widget->setUserData(MWWorld::Ptr(spell.mItem)); widget->setUserString("ToolTipType", "ItemPtr"); } else { widget->setUserString("ToolTipType", "Spell"); widget->setUserString("Spell", spell.mId.serialize()); } widget->setUserString(sSpellModelIndex, MyGUI::utility::toString(index)); widget->eventMouseWheel += MyGUI::newDelegate(this, &SpellView::onMouseWheelMoved); widget->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellView::onSpellSelected); } SpellModel::ModelIndex SpellView::getSpellModelIndex(MyGUI::Widget* widget) { return MyGUI::utility::parseInt(widget->getUserString(sSpellModelIndex)); } void SpellView::onSpellSelected(MyGUI::Widget* _sender) { eventSpellClicked(getSpellModelIndex(_sender)); } void SpellView::onMouseWheelMoved(MyGUI::Widget* _sender, int _rel) { 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))); } void SpellView::resetScrollbars() { mScrollView->setViewOffset(MyGUI::IntPoint(0, 0)); } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellview.hpp000066400000000000000000000051401503074453300225430ustar00rootroot00000000000000#ifndef OPENMW_GUI_SPELLVIEW_H #define OPENMW_GUI_SPELLVIEW_H #include #include #include #include "spellmodel.hpp" namespace MyGUI { class ScrollView; } namespace MWGui { class SpellModel; ///@brief Displays a SpellModel in a list widget class SpellView final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(SpellView) public: SpellView(); /// Register needed components with MyGUI's factory manager static void registerComponents(); /// Should the cost/chance column be shown? void setShowCostColumn(bool show); void setHighlightSelected(bool highlight); /// Takes ownership of \a model void setModel(SpellModel* model); SpellModel* getModel(); void update(); /// simplified update called each frame void incrementalUpdate(); typedef MyGUI::delegates::MultiDelegate EventHandle_ModelIndex; /// Fired when a spell was clicked EventHandle_ModelIndex eventSpellClicked; void initialiseOverride() override; void setSize(const MyGUI::IntSize& _value) override; void setCoord(const MyGUI::IntCoord& _value) override; void resetScrollbars(); private: MyGUI::ScrollView* mScrollView; std::unique_ptr mModel; /// tracks a row in the spell view struct LineInfo { /// the widget on the left side of the row MyGUI::Widget* mLeftWidget; /// the widget on the left side of the row (if there is one) MyGUI::Widget* mRightWidget; /// index to item in mModel that row is showing information for SpellModel::ModelIndex mSpellIndex; LineInfo(MyGUI::Widget* leftWidget, MyGUI::Widget* rightWidget, SpellModel::ModelIndex spellIndex); }; /// magic number indicating LineInfo does not correspond to an item in mModel enum { NoSpellIndex = -1 }; std::vector mLines; bool mShowCostColumn; bool mHighlightSelected; void layoutWidgets(); void addGroup(const std::string& label1, const std::string& label2); void adjustSpellWidget(const Spell& spell, SpellModel::ModelIndex index, MyGUI::Widget* widget); void onSpellSelected(MyGUI::Widget* _sender); void onMouseWheelMoved(MyGUI::Widget* _sender, int _rel); SpellModel::ModelIndex getSpellModelIndex(MyGUI::Widget* _sender); static const char* sSpellModelIndex; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/spellwindow.cpp000066400000000000000000000224601503074453300230770ustar00rootroot00000000000000#include "spellwindow.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spells.hpp" #include "../mwmechanics/spellutil.hpp" #include "confirmationdialog.hpp" #include "spellicons.hpp" #include "spellview.hpp" namespace MWGui { SpellWindow::SpellWindow(DragAndDrop* drag) : WindowPinnableBase("openmw_spell_window.layout") , NoDrop(drag, mMainWidget) , mSpellView(nullptr) , mUpdateTimer(0.0f) { mSpellIcons = std::make_unique(); MyGUI::Widget* deleteButton; getWidget(deleteButton, "DeleteSpellButton"); getWidget(mSpellView, "SpellView"); getWidget(mEffectBox, "EffectsBox"); getWidget(mFilterEdit, "FilterEdit"); mSpellView->eventSpellClicked += MyGUI::newDelegate(this, &SpellWindow::onModelIndexSelected); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &SpellWindow::onFilterChanged); deleteButton->eventMouseButtonClick += MyGUI::newDelegate(this, &SpellWindow::onDeleteClicked); setCoord(498, 300, 302, 300); // Adjust the spell filtering widget size because of MyGUI limitations. int filterWidth = mSpellView->getSize().width - deleteButton->getSize().width - 3; mFilterEdit->setSize(filterWidth, mFilterEdit->getSize().height); } void SpellWindow::onPinToggled() { Settings::windows().mSpellsPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setSpellVisibility(!mPinned); } void SpellWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Magic); } void SpellWindow::onOpen() { // Reset the filter focus when opening the window MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); if (focus == mFilterEdit) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(nullptr); updateSpells(); } void SpellWindow::onFrame(float dt) { NoDrop::onFrame(dt); mUpdateTimer += dt; if (0.5f < mUpdateTimer) { mUpdateTimer = 0; mSpellView->incrementalUpdate(); } // Update effects if the time is unpaused for any reason (e.g. the window is pinned) if (!MWBase::Environment::get().getWorld()->getTimeManager()->isPaused()) mSpellIcons->updateWidgets(mEffectBox, false); } void SpellWindow::updateSpells() { mSpellIcons->updateWidgets(mEffectBox, false); mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), mFilterEdit->getCaption())); } void SpellWindow::onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped) { MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::InventoryStore& store = player.getClass().getInventoryStore(player); // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = store.begin(); for (; it != store.end(); ++it) { if (*it == item) { break; } } if (it == store.end()) 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()) { MWBase::Environment::get().getWindowManager()->useItem(item); // make sure that item was successfully equipped if (!store.isEquipped(item)) return; } store.setSelectedEnchantItem(it); // to reset WindowManager::mSelectedSpell immediately MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it); updateSpells(); } void SpellWindow::askDeleteSpell(const ESM::RefId& spellId) { // delete spell, if allowed const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(spellId); MWWorld::Ptr player = MWMechanics::getPlayer(); 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 ESM::RefId& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!isInherent && !signId.empty()) { 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) { windowManager->messageBox("#{sDeleteSpellError}"); } else { // ask for confirmation mSpellToDelete = spellId; 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(); dialog->eventOkClicked += MyGUI::newDelegate(this, &SpellWindow::onDeleteSpellAccept); dialog->eventCancelClicked.clear(); } } void SpellWindow::onModelIndexSelected(SpellModel::ModelIndex index) { const Spell& spell = mSpellView->getModel()->getItem(index); if (spell.mType == Spell::Type_EnchantedItem) { onEnchantedItemSelected(spell.mItem, spell.mActive); } else { if (MyGUI::InputManager::getInstance().isShiftPressed()) askDeleteSpell(spell.mId); else onSpellSelected(spell.mId); } } void SpellWindow::onFilterChanged(MyGUI::EditBox* sender) { mSpellView->setModel(new SpellModel(MWMechanics::getPlayer(), sender->getCaption())); } void SpellWindow::onDeleteClicked(MyGUI::Widget* widget) { SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) return; const Spell& spell = mSpellView->getModel()->getItem(selected); if (spell.mType != Spell::Type_EnchantedItem) askDeleteSpell(spell.mId); } 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))); updateSpells(); } void SpellWindow::onDeleteSpellAccept() { MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWMechanics::Spells& spells = stats.getSpells(); if (MWBase::Environment::get().getWindowManager()->getSelectedSpell() == mSpellToDelete) MWBase::Environment::get().getWindowManager()->unsetSelectedSpell(); spells.remove(mSpellToDelete); updateSpells(); } void SpellWindow::cycle(bool next) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(player)) return; const MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead() || stats.getHitRecovery()) return; mSpellView->setModel(new SpellModel(MWMechanics::getPlayer())); SpellModel::ModelIndex selected = mSpellView->getModel()->getSelectedIndex(); if (selected < 0) selected = 0; selected += next ? 1 : -1; int itemcount = mSpellView->getModel()->getItemCount(); if (itemcount == 0) return; selected = (selected + itemcount) % itemcount; const Spell& spell = mSpellView->getModel()->getItem(selected); if (spell.mType == Spell::Type_EnchantedItem) onEnchantedItemSelected(spell.mItem, spell.mActive); else onSpellSelected(spell.mId); } } openmw-openmw-0.49.0/apps/openmw/mwgui/spellwindow.hpp000066400000000000000000000024661503074453300231100ustar00rootroot00000000000000#ifndef MWGUI_SPELLWINDOW_H #define MWGUI_SPELLWINDOW_H #include #include "spellicons.hpp" #include "spellmodel.hpp" #include "windowpinnablebase.hpp" namespace MWGui { class SpellView; class SpellWindow : public WindowPinnableBase, public NoDrop { public: SpellWindow(DragAndDrop* drag); void updateSpells(); void onFrame(float dt) override; /// Cycle to next/previous spell void cycle(bool next); std::string_view getWindowIdForLua() const override { return "Magic"; } protected: MyGUI::Widget* mEffectBox; ESM::RefId mSpellToDelete; void onEnchantedItemSelected(MWWorld::Ptr item, bool alreadyEquipped); void onSpellSelected(const ESM::RefId& spellId); void onModelIndexSelected(SpellModel::ModelIndex index); void onFilterChanged(MyGUI::EditBox* sender); void onDeleteClicked(MyGUI::Widget* widget); void onDeleteSpellAccept(); void askDeleteSpell(const ESM::RefId& spellId); void onPinToggled() override; void onTitleDoubleClicked() override; void onOpen() override; SpellView* mSpellView; std::unique_ptr mSpellIcons; MyGUI::EditBox* mFilterEdit; private: float mUpdateTimer; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/statswatcher.cpp000066400000000000000000000143731503074453300232500ustar00rootroot00000000000000#include "statswatcher.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.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) { } void StatsWatcher::watchActor(const MWWorld::Ptr& ptr) { mWatched = ptr; } void StatsWatcher::update() { if (mWatched.isEmpty()) return; 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()) { const auto& value = stats.getAttribute(attribute.mId); if (value != mWatchedAttributes[attribute.mId] || mWatchedStatsEmpty) { mWatchedAttributes[attribute.mId] = value; setAttribute(attribute.mId, value); } } if (stats.getHealth() != mWatchedHealth || mWatchedStatsEmpty) { mWatchedHealth = stats.getHealth(); setValue("HBar", stats.getHealth()); } if (stats.getMagicka() != mWatchedMagicka || mWatchedStatsEmpty) { mWatchedMagicka = stats.getMagicka(); setValue("MBar", stats.getMagicka()); } if (stats.getFatigue() != mWatchedFatigue || mWatchedStatsEmpty) { mWatchedFatigue = stats.getFatigue(); setValue("FBar", stats.getFatigue()); } float timeToDrown = stats.getTimeToStartDrowning(); if (timeToDrown != mWatchedTimeToStartDrowning) { 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 winMgr->setDrowningBarVisibility(false); else { winMgr->setDrowningBarVisibility(true); winMgr->setDrowningTimeLeft(stats.getTimeToStartDrowning(), fHoldBreathTime); } } for (const ESM::Skill& skill : store->get()) { const auto& value = stats.getSkill(skill.mId); if (value != mWatchedSkills[skill.mId] || mWatchedStatsEmpty) { mWatchedSkills[skill.mId] = value; setValue(skill.mId, value); } } if (stats.getLevel() != mWatchedLevel || mWatchedStatsEmpty) { mWatchedLevel = stats.getLevel(); setValue("level", mWatchedLevel); } if (mWatched.getClass().isNpc()) { const ESM::NPC* watchedRecord = mWatched.get()->mBase; if (watchedRecord->mName != mWatchedName || mWatchedStatsEmpty) { mWatchedName = watchedRecord->mName; setValue("name", watchedRecord->mName); } if (watchedRecord->mRace != mWatchedRace || mWatchedStatsEmpty) { mWatchedRace = 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 = store->get().find(watchedRecord->mClass); setValue("class", cls->mName); size_t size = cls->mData.mSkills.size(); std::vector majorSkills(size); std::vector minorSkills(size); for (size_t i = 0; i < size; ++i) { minorSkills[i] = ESM::Skill::indexToRefId(cls->mData.mSkills[i][0]); majorSkills[i] = ESM::Skill::indexToRefId(cls->mData.mSkills[i][1]); } configureSkills(majorSkills, minorSkills); } } mWatchedStatsEmpty = false; } void StatsWatcher::addListener(StatsListener* listener) { mListeners.insert(listener); } void StatsWatcher::removeListener(StatsListener* listener) { mListeners.erase(listener); } void StatsWatcher::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { for (StatsListener* listener : mListeners) listener->setAttribute(id, value); } void StatsWatcher::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::setValue(std::string_view id, const MWMechanics::DynamicStat& value) { for (StatsListener* listener : mListeners) listener->setValue(id, value); } void StatsWatcher::setValue(std::string_view id, const std::string& value) { for (StatsListener* listener : mListeners) listener->setValue(id, 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) { for (StatsListener* listener : mListeners) listener->configureSkills(major, minor); } } openmw-openmw-0.49.0/apps/openmw/mwgui/statswatcher.hpp000066400000000000000000000045341503074453300232530ustar00rootroot00000000000000#ifndef MWGUI_STATSWATCHER_H #define MWGUI_STATSWATCHER_H #include #include #include #include #include "../mwmechanics/stat.hpp" #include "../mwworld/ptr.hpp" namespace MWGui { class StatsListener { public: virtual ~StatsListener() = default; /// Set value for the given ID. 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; std::map mWatchedAttributes; std::map mWatchedSkills; MWMechanics::DynamicStat mWatchedHealth; MWMechanics::DynamicStat mWatchedMagicka; MWMechanics::DynamicStat mWatchedFatigue; std::string mWatchedName; ESM::RefId mWatchedRace; ESM::RefId mWatchedClass; int mWatchedLevel; float mWatchedTimeToStartDrowning; bool mWatchedStatsEmpty; std::set mListeners; 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(); void update(); void addListener(StatsListener* listener); void removeListener(StatsListener* listener); void watchActor(const MWWorld::Ptr& ptr); MWWorld::Ptr getWatchedActor() const { return mWatched; } void forceUpdate() { mWatchedStatsEmpty = true; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/statswindow.cpp000066400000000000000000000742531503074453300231250ustar00rootroot00000000000000#include "statswindow.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 "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.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) , mReputation(0) , mBounty(0) , mChanged(true) , mMinFullWidth(mMainWidget->getSize().width) { 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()) { 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 (const ESM::Skill& skill : store.get()) { mSkillValues.emplace(skill.mId, MWMechanics::SkillValue()); mSkillWidgetMap.emplace(skill.mId, std::make_pair(nullptr, nullptr)); } MyGUI::Window* t = mMainWidget->castType(); t->eventWindowChangeCoord += MyGUI::newDelegate(this, &StatsWindow::onWindowResize); onWindowResize(t); } void StatsWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { 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))); } void StatsWindow::onWindowResize(MyGUI::Window* window) { 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 float leftPaneRatio = 0.44f; if (mLeftPane->isUserString("LeftPaneRatio")) leftPaneRatio = MyGUI::utility::parseFloat(mLeftPane->getUserString("LeftPaneRatio")); int leftOffsetWidth = 24; if (mLeftPane->isUserString("LeftOffsetWidth")) leftOffsetWidth = MyGUI::utility::parseInt(mLeftPane->getUserString("LeftOffsetWidth")); float rightPaneRatio = 1.f - leftPaneRatio; int minLeftWidth = static_cast(mMinFullWidth * leftPaneRatio); int minLeftOffsetWidth = minLeftWidth + leftOffsetWidth; // 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 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 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)); } // 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->setVisibleVScroll(true); } void StatsWindow::setBar(const std::string& name, const std::string& tname, int val, int max) { MyGUI::ProgressBar* pt; getWidget(pt, name); std::stringstream out; out << val << "/" << max; setText(tname, out.str()); pt->setProgressRange(std::max(0, max)); pt->setProgressPosition(std::max(0, val)); } void StatsWindow::setPlayerName(const std::string& playerName) { mMainWidget->castType()->setCaption(playerName); } void StatsWindow::setAttribute(ESM::RefId id, const MWMechanics::AttributeValue& value) { auto it = mAttributeWidgets.find(id); if (it != mAttributeWidgets.end()) { 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(std::string_view id, const MWMechanics::DynamicStat& value) { int current = static_cast(value.getCurrent()); int modified = static_cast(value.getModified(false)); // Fatigue can be negative if (id != "FBar") current = std::max(0, current); 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); if (id == "HBar") { getWidget(w, "Health"); w->setUserString("Caption_HealthDescription", "#{sHealthDesc}\n" + valStr); } else if (id == "MBar") { getWidget(w, "Magicka"); w->setUserString("Caption_HealthDescription", "#{sMagDesc}\n" + valStr); } else if (id == "FBar") { getWidget(w, "Fatigue"); w->setUserString("Caption_HealthDescription", "#{sFatDesc}\n" + valStr); } } 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); } void StatsWindow::setValue(std::string_view id, int value) { if (id == "level") { std::ostringstream text; text << value; setText("LevelText", text.str()); } } void setSkillProgress(MyGUI::Widget* w, float progress, ESM::RefId skillId) { MWWorld::Ptr player = MWMechanics::getPlayer(); const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); 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 // reach 100% without the skill levelling up. // 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("RangePosition_SkillProgress", MyGUI::utility::toString(progressPercent)); } void StatsWindow::setValue(ESM::RefId id, const MWMechanics::SkillValue& value) { mSkillValues[id] = value; std::pair widgets = mSkillWidgetMap[id]; MyGUI::TextBox* valueWidget = widgets.second; MyGUI::TextBox* nameWidget = widgets.first; if (valueWidget && nameWidget) { int modified = value.getModified(), base = value.getBase(); std::string text = MyGUI::utility::toString(modified); std::string state = "normal"; if (modified > base) state = "increased"; else if (modified < base) state = "decreased"; int widthBefore = valueWidget->getTextSize().width; valueWidget->setCaption(text); valueWidget->_setWidgetState(state); 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()); } if (value.getBase() < 100) { nameWidget->setUserString("Visible_SkillMaxed", "false"); nameWidget->setUserString("UserData^Hidden_SkillMaxed", "true"); nameWidget->setUserString("Visible_SkillProgressVBox", "true"); nameWidget->setUserString("UserData^Hidden_SkillProgressVBox", "false"); valueWidget->setUserString("Visible_SkillMaxed", "false"); valueWidget->setUserString("UserData^Hidden_SkillMaxed", "true"); valueWidget->setUserString("Visible_SkillProgressVBox", "true"); valueWidget->setUserString("UserData^Hidden_SkillProgressVBox", "false"); setSkillProgress(nameWidget, value.getProgress(), id); setSkillProgress(valueWidget, value.getProgress(), id); } else { nameWidget->setUserString("Visible_SkillMaxed", "true"); nameWidget->setUserString("UserData^Hidden_SkillMaxed", "false"); nameWidget->setUserString("Visible_SkillProgressVBox", "false"); nameWidget->setUserString("UserData^Hidden_SkillProgressVBox", "true"); valueWidget->setUserString("Visible_SkillMaxed", "true"); valueWidget->setUserString("UserData^Hidden_SkillMaxed", "false"); valueWidget->setUserString("Visible_SkillProgressVBox", "false"); valueWidget->setUserString("UserData^Hidden_SkillProgressVBox", "true"); } } } 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::copy(major.begin(), major.end(), std::inserter(skillSet, skillSet.begin())); std::copy(minor.begin(), minor.end(), std::inserter(skillSet, skillSet.begin())); mMiscSkills.clear(); const auto& store = MWBase::Environment::get().getWorld()->getStore().get(); for (const auto& skill : store) { if (!skillSet.contains(skill.mId)) mMiscSkills.push_back(skill.mId); } updateSkillArea(); } void StatsWindow::onFrame(float dt) { NoDrop::onFrame(dt); MWWorld::Ptr player = MWMechanics::getPlayer(); 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) { 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("Range_LevelProgress", MyGUI::utility::toString(max)); levelWidget->setUserString("Caption_LevelProgressText", MyGUI::utility::toString(PCstats.getLevelProgress()) + "/" + MyGUI::utility::toString(max)); levelWidget->setUserString("Caption_LevelDetailText", detailText); } setFactions(PCstats.getFactionRanks()); setExpelled(PCstats.getExpelled()); const auto& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); setBirthSign(signId); setReputation(PCstats.getReputation()); setBounty(PCstats.getBounty()); if (mChanged) updateSkillArea(); } void StatsWindow::setFactions(const FactionList& factions) { if (mFactions != factions) { mFactions = factions; mChanged = true; } } void StatsWindow::setExpelled(const std::set& expelled) { if (mExpelled != expelled) { mExpelled = expelled; mChanged = true; } } void StatsWindow::setBirthSign(const ESM::RefId& signId) { if (signId != mBirthSignId) { mBirthSignId = signId; mChanged = true; } } 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), MyGUI::Align::Left | MyGUI::Align::Top | MyGUI::Align::HStretch); separator->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(separator); coord1.top += separator->getHeight(); coord2.top += separator->getHeight(); } 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(MyGUI::UString(label)); groupWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); mSkillWidgets.push_back(groupWidget); const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; } 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(MyGUI::UString(text)); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); 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); skillNameWidget->setSize(skillNameWidget->getSize() + MyGUI::IntSize(coord2.width - textWidthPlusMargin, 0)); mSkillWidgets.push_back(skillNameWidget); mSkillWidgets.push_back(skillValueWidget); 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::TextBox* skillNameWidget; skillNameWidget = mSkillView->createWidget("SandText", coord1, MyGUI::Align::Default); skillNameWidget->setCaption(text); skillNameWidget->eventMouseWheel += MyGUI::newDelegate(this, &StatsWindow::onMouseWheel); int textWidth = skillNameWidget->getTextSize().width; skillNameWidget->setSize(textWidth, skillNameWidget->getHeight()); mSkillWidgets.push_back(skillNameWidget); const int lineHeight = Settings::gui().mFontSize + 2; coord1.top += lineHeight; coord2.top += lineHeight; return skillNameWidget; } 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()) { addSeparator(coord1, coord2); } addGroup( MWBase::Environment::get().getWindowManager()->getGameSettingString(titleId, titleDefault), coord1, coord2); const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); for (const ESM::RefId& skillId : skills) { const ESM::Skill* skill = esmStore.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 window: can not find value for skill " << skill->mId; continue; } const ESM::Attribute* attr = esmStore.get().find(ESM::Attribute::indexToRefId(skill->mData.mAttribute)); std::pair widgets = addValueItem(skill->mName, {}, "normal", coord1, coord2); mSkillWidgetMap[skill->mId] = std::move(widgets); 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", 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(skill->mId, skillValue->second); } } void StatsWindow::updateSkillArea() { mChanged = false; for (MyGUI::Widget* widget : mSkillWidgets) { MyGUI::Gui::getInstance().destroyWidget(widget); } mSkillWidgets.clear(); const int valueSize = 40; MyGUI::IntCoord coord1(10, 0, mSkillView->getWidth() - (10 + valueSize) - 24, 18); MyGUI::IntCoord coord2(coord1.left + coord1.width, coord1.top, valueSize, coord1.height); if (!mMajorSkills.empty()) addSkills(mMajorSkills, "sSkillClassMajor", "Major Skills", coord1, coord2); if (!mMinorSkills.empty()) addSkills(mMinorSkills, "sSkillClassMinor", "Minor Skills", coord1, coord2); 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; // race tooltip const ESM::Race* playerRace = store.get().find(player->mRace); MyGUI::Widget* raceWidget; getWidget(raceWidget, "RaceText"); ToolTips::createRaceToolTip(raceWidget, playerRace); getWidget(raceWidget, "Race_str"); ToolTips::createRaceToolTip(raceWidget, playerRace); // class tooltip MyGUI::Widget* classWidget; const ESM::Class* playerClass = store.get().find(player->mClass); getWidget(classWidget, "ClassText"); ToolTips::createClassToolTip(classWidget, *playerClass); getWidget(classWidget, "Class_str"); ToolTips::createClassToolTip(classWidget, *playerClass); if (!mFactions.empty()) { MWWorld::Ptr playerPtr = MWMechanics::getPlayer(); const MWMechanics::NpcStats& PCstats = playerPtr.getClass().getNpcStats(playerPtr); const std::set& expelled = PCstats.getExpelled(); bool firstFaction = true; for (const auto& [factionId, factionRank] : mFactions) { const ESM::Faction* faction = store.get().find(factionId); if (faction->mData.mIsHidden == 1) continue; if (firstFaction) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addGroup(MWBase::Environment::get().getWindowManager()->getGameSettingString("sFaction", "Faction"), coord1, coord2); firstFaction = false; } MyGUI::Widget* w = addItem(faction->mName, coord1, coord2); std::string text; text += std::string("#{fontcolourhtml=header}") + faction->mName; if (expelled.find(factionId) != expelled.end()) text += "\n#{fontcolourhtml=normal}#{sExpelled}"; else { 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]; 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}" + 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 id : faction->mData.mSkills) { const ESM::Skill* skill = store.get().search(ESM::Skill::indexToRefId(id)); if (skill) { if (!firstSkill) text += ", "; firstSkill = false; text += MyGUI::TextIterator::toTagsString(skill->mName); } } text += "\n"; if (rankData.mPrimarySkill > 0) text += "\n#{sNeedOneSkill} " + MyGUI::utility::toString(rankData.mPrimarySkill); if (rankData.mFavouredSkill > 0) text += " #{sand} #{sNeedTwoSkills} " + MyGUI::utility::toString(rankData.mFavouredSkill); } } w->setUserString("ToolTipType", "Layout"); w->setUserString("ToolTipLayout", "FactionToolTip"); w->setUserString("Caption_FactionText", text); } } if (!mBirthSignId.empty()) { // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); 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); } // Add a line separator if there are items above if (!mSkillWidgets.empty()) addSeparator(coord1, coord2); addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sReputation", "Reputation"), MyGUI::utility::toString(static_cast(mReputation)), "normal", coord1, coord2); 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}"); } addValueItem(MWBase::Environment::get().getWindowManager()->getGameSettingString("sBounty", "Bounty"), MyGUI::utility::toString(static_cast(mBounty)), "normal", coord1, coord2); 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}"); } // 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->setVisibleVScroll(true); } void StatsWindow::onPinToggled() { Settings::windows().mStatsPin.set(mPinned); MWBase::Environment::get().getWindowManager()->setHMSVisibility(!mPinned); } void StatsWindow::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) { MWBase::Environment::get().getWindowManager()->toggleMaximized(this); MyGUI::Window* t = mMainWidget->castType(); onWindowResize(t); } else if (!mPinned) MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Stats); } } openmw-openmw-0.49.0/apps/openmw/mwgui/statswindow.hpp000066400000000000000000000072031503074453300231210ustar00rootroot00000000000000#ifndef MWGUI_STATS_WINDOW_H #define MWGUI_STATS_WINDOW_H #include "statswatcher.hpp" #include "windowpinnablebase.hpp" #include #include namespace MWGui { class StatsWindow : public WindowPinnableBase, public NoDrop, public StatsListener { 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 openmw-openmw-0.49.0/apps/openmw/mwgui/textcolours.cpp000066400000000000000000000021511503074453300231160ustar00rootroot00000000000000#include "textcolours.hpp" #include #include namespace MWGui { MyGUI::Colour getTextColour(const std::string& type) { return MyGUI::Colour::parse(MyGUI::LanguageManager::getInstance().replaceTags("#{fontcolour=" + type + "}")); } void TextColours::loadColours() { header = getTextColour("header"); normal = getTextColour("normal"); notify = getTextColour("notify"); link = getTextColour("link"); linkOver = getTextColour("link_over"); linkPressed = getTextColour("link_pressed"); answer = getTextColour("answer"); answerOver = getTextColour("answer_over"); answerPressed = getTextColour("answer_pressed"); journalLink = getTextColour("journal_link"); journalLinkOver = getTextColour("journal_link_over"); journalLinkPressed = getTextColour("journal_link_pressed"); journalTopic = getTextColour("journal_topic"); journalTopicOver = getTextColour("journal_topic_over"); journalTopicPressed = getTextColour("journal_topic_pressed"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/textcolours.hpp000066400000000000000000000013111503074453300231200ustar00rootroot00000000000000#ifndef MWGUI_TEXTCOLORS_H #define MWGUI_TEXTCOLORS_H #include namespace MWGui { struct TextColours { MyGUI::Colour header; MyGUI::Colour normal; MyGUI::Colour notify; MyGUI::Colour link; MyGUI::Colour linkOver; MyGUI::Colour linkPressed; MyGUI::Colour answer; MyGUI::Colour answerOver; MyGUI::Colour answerPressed; MyGUI::Colour journalLink; MyGUI::Colour journalLinkOver; MyGUI::Colour journalLinkPressed; MyGUI::Colour journalTopic; MyGUI::Colour journalTopicOver; MyGUI::Colour journalTopicPressed; public: void loadColours(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/textinput.cpp000066400000000000000000000046331503074453300225760ustar00rootroot00000000000000#include "textinput.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include #include namespace MWGui { TextInputDialog::TextInputDialog() : WindowModal("openmw_text_input.layout") { // Centre dialog center(); getWidget(mTextEdit, "TextEdit"); mTextEdit->eventEditSelectAccept += newDelegate(this, &TextInputDialog::onTextAccepted); MyGUI::Button* okButton; getWidget(okButton, "OKButton"); okButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TextInputDialog::onOkClicked); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } void TextInputDialog::setNextButtonShow(bool shown) { MyGUI::Button* okButton; getWidget(okButton, "OKButton"); if (shown) okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sNext", {}))); else okButton->setCaption( MyGUI::UString(MWBase::Environment::get().getWindowManager()->getGameSettingString("sOK", {}))); } void TextInputDialog::setTextLabel(std::string_view label) { setText("LabelT", label); } void TextInputDialog::onOpen() { WindowModal::onOpen(); // Make sure the edit box has focus MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } // widget controls void TextInputDialog::onOkClicked(MyGUI::Widget* _sender) { if (mTextEdit->getCaption().empty()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage37}"); MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mTextEdit); } else eventDone(this); } void TextInputDialog::onTextAccepted(MyGUI::EditBox* _sender) { onOkClicked(_sender); // To do not spam onTextAccepted() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } std::string TextInputDialog::getTextInput() const { return mTextEdit->getCaption(); } void TextInputDialog::setTextInput(const std::string& text) { mTextEdit->setCaption(text); } } openmw-openmw-0.49.0/apps/openmw/mwgui/textinput.hpp000066400000000000000000000014401503074453300225740ustar00rootroot00000000000000#ifndef MWGUI_TEXT_INPUT_H #define MWGUI_TEXT_INPUT_H #include "windowbase.hpp" namespace MWGui { class TextInputDialog : public WindowModal { public: TextInputDialog(); std::string getTextInput() const; void setTextInput(const std::string& text); void setNextButtonShow(bool shown); void setTextLabel(std::string_view label); void onOpen() override; bool exit() override { return false; } /** Event : Dialog finished, OK button clicked.\n signature : void method()\n */ EventHandle_WindowBase eventDone; protected: void onOkClicked(MyGUI::Widget* _sender); void onTextAccepted(MyGUI::EditBox* _sender); private: MyGUI::EditBox* mTextEdit; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/timeadvancer.cpp000066400000000000000000000024051503074453300231670ustar00rootroot00000000000000#include "timeadvancer.hpp" namespace MWGui { TimeAdvancer::TimeAdvancer(float delay) : mRunning(false) , mCurHour(0) , mHours(1) , mInterruptAt(-1) , mDelay(delay) , mRemainingTime(delay) { } void TimeAdvancer::run(int hours, int interruptAt) { mHours = hours; mCurHour = 0; mInterruptAt = interruptAt; mRemainingTime = mDelay; mRunning = true; } void TimeAdvancer::stop() { mRunning = false; } void TimeAdvancer::onFrame(float dt) { if (!mRunning) return; if (mCurHour == mInterruptAt) { stop(); eventInterrupted(); return; } mRemainingTime -= dt; while (mRemainingTime <= 0) { mRemainingTime += mDelay; ++mCurHour; if (mCurHour <= mHours) eventProgressChanged(mCurHour, mHours); else { stop(); eventFinished(); return; } } } int TimeAdvancer::getHours() const { return mHours; } bool TimeAdvancer::isRunning() const { return mRunning; } } openmw-openmw-0.49.0/apps/openmw/mwgui/timeadvancer.hpp000066400000000000000000000014711503074453300231760ustar00rootroot00000000000000#ifndef MWGUI_TIMEADVANCER_H #define MWGUI_TIMEADVANCER_H #include namespace MWGui { class TimeAdvancer { public: TimeAdvancer(float delay); void run(int hours, int interruptAt = -1); void stop(); void onFrame(float dt); int getHours() const; bool isRunning() const; // signals typedef MyGUI::delegates::MultiDelegate<> EventHandle_Void; typedef MyGUI::delegates::MultiDelegate EventHandle_IntInt; EventHandle_IntInt eventProgressChanged; EventHandle_Void eventInterrupted; EventHandle_Void eventFinished; private: bool mRunning; int mCurHour; int mHours; int mInterruptAt; float mDelay; float mRemainingTime; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/tooltips.cpp000066400000000000000000001175621503074453300224150ustar00rootroot00000000000000#include "tooltips.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.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 "inventorywindow.hpp" #include "mapwindow.hpp" #include "itemmodel.hpp" namespace MWGui { ToolTips::ToolTips() : Layout("openmw_tooltips.layout") , mFocusToolTipX(0.0) , mFocusToolTipY(0.0) , mHorizontalScrollIndex(0) , mRemainingDelay(Settings::gui().mTooltipDelay) , mLastMouseX(0) , mLastMouseY(0) , mEnabled(true) , mFullHelp(false) , mFrameDuration(0.f) { getWidget(mDynamicToolTipBox, "DynamicToolTipBox"); mDynamicToolTipBox->setVisible(false); // turn off mouse focus so that getMouseFocusWidget returns the correct widget, // even if the mouse is over the tooltip mDynamicToolTipBox->setNeedMouseFocus(false); mMainWidget->setNeedMouseFocus(false); for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } } void ToolTips::setEnabled(bool enabled) { mEnabled = enabled; } void ToolTips::onFrame(float frameDuration) { mFrameDuration = frameDuration; } void ToolTips::update(float frameDuration) { while (mDynamicToolTipBox->getChildCount()) { MyGUI::Gui::getInstance().destroyWidget(mDynamicToolTipBox->getChildAt(0)); } // start by hiding everything for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); if (!mEnabled) { return; } MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); bool guiMode = winMgr->isGuiMode(); if (guiMode) { if (!winMgr->getCursorVisible()) return; const MyGUI::IntPoint& mousePos = MyGUI::InputManager::getInstance().getMousePosition(); if (winMgr->getWorldMouseOver() && (winMgr->isConsoleMode() || (winMgr->getMode() == GM_Container) || (winMgr->getMode() == GM_Inventory))) { if (mFocusObject.isEmpty()) return; const MWWorld::Class& objectclass = mFocusObject.getClass(); MyGUI::IntSize tooltipSize; if (!objectclass.hasToolTip(mFocusObject) && winMgr->isConsoleMode()) { setCoord(0, 0, 300, 300); mDynamicToolTipBox->setVisible(true); ToolTipInfo info; info.caption = mFocusObject.getClass().getName(mFocusObject); if (info.caption.empty()) info.caption = mFocusObject.getCellRef().getRefId().toDebugString(); info.icon.clear(); tooltipSize = createToolTip(info, checkOwned()); } else tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); setCoord(tooltipPosition.left, tooltipPosition.top, tooltipSize.width, tooltipSize.height); } else { if (mousePos.left == mLastMouseX && mousePos.top == mLastMouseY) { mRemainingDelay -= frameDuration; } else { mHorizontalScrollIndex = 0; mRemainingDelay = Settings::gui().mTooltipDelay; } mLastMouseX = mousePos.left; mLastMouseY = mousePos.top; if (mRemainingDelay > 0) return; MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); if (focus == nullptr) return; MyGUI::IntSize tooltipSize; // 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 while (!focus->isUserString("ToolTipType")) { focus = focus->getParent(); if (!focus) return; } std::string_view type = focus->getUserString("ToolTipType"); 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") { LocalMapBase::MarkerUserData data = *focus->getUserData(); if (!data.isPositionExplored()) return; ToolTipInfo info; info.text = data.caption; info.notes = data.notes; tooltipSize = createToolTip(info); } else if (type == "ItemPtr") { mFocusObject = *focus->getUserData(); if (mFocusObject.isEmpty()) return; tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false, checkOwned()); } else if (type == "ItemModelIndex") { 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); } else if (type == "ToolTipInfo") { tooltipSize = createToolTip(*focus->getUserData()); } 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); mFocusObject = item; if (!mFocusObject.isEmpty()) tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), false); } else if (type == "Spell") { ToolTipInfo info; 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::IndexedENAMstruct& spellEffect : spell->mEffects.mList) { Widgets::SpellEffectParams params; 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); } // display school of spells that contribute to skill progress if (MWMechanics::spellIncreasesSkill(spell)) { 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(); } } 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") { // tooltip defined in the layout MyGUI::Widget* tooltip; getWidget(tooltip, focus->getUserString("ToolTipLayout")); tooltip->setVisible(true); 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_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 = first.substr(0, caretPos); key.erase(key.begin(), key.begin() + caretPos + 1); } MyGUI::Widget* w; getWidget(w, widgetName); if (type == "Property") w->setProperty(key, userStringPair.second); else if (type == "UserData") w->setUserString(key, userStringPair.second); } tooltipSize = tooltip->getSize(); tooltip->setCoord(0, 0, tooltipSize.width, tooltipSize.height); } else throw std::runtime_error("unknown tooltip type"); MyGUI::IntPoint tooltipPosition = MyGUI::InputManager::getInstance().getMousePosition(); position(tooltipPosition, tooltipSize, viewSize); setCoord(tooltipPosition.left, tooltipPosition.top, tooltipSize.width, tooltipSize.height); } } else { if (!mFocusObject.isEmpty()) { MyGUI::IntSize tooltipSize = getToolTipViaPtr(mFocusObject.getCellRef().getCount(), true, checkOwned()); const int left = viewSize.width / 2 - tooltipSize.width / 2; const int top = std::max(0, int(mFocusToolTipY * viewSize.height - tooltipSize.height - 20)); setCoord(left, top, tooltipSize.width, tooltipSize.height); mDynamicToolTipBox->setVisible(true); } } } 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); if ((position.left + size.width) > viewportSize.width) { position.left = viewportSize.width - size.width; } if ((position.top + size.height) > viewportSize.height) { position.top = MyGUI::InputManager::getInstance().getMousePosition().top - size.height - 8; } } void ToolTips::clear() { mFocusObject = MWWorld::Ptr(); while (mDynamicToolTipBox->getChildCount()) { MyGUI::Gui::getInstance().destroyWidget(mDynamicToolTipBox->getChildAt(0)); } for (unsigned int i = 0; i < mMainWidget->getChildCount(); ++i) { mMainWidget->getChildAt(i)->setVisible(false); } } void ToolTips::setFocusObject(const MWWorld::Ptr& focus) { mFocusObject = focus; update(mFrameDuration); } 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); MyGUI::IntSize tooltipSize; const MWWorld::Class& object = mFocusObject.getClass(); if (!object.hasToolTip(mFocusObject)) { mDynamicToolTipBox->setVisible(false); } else { mDynamicToolTipBox->setVisible(true); ToolTipInfo info = object.getToolTipInfo(mFocusObject, count); if (!image) info.icon.clear(); tooltipSize = createToolTip(info, isOwned); } return tooltipSize; } bool ToolTips::checkOwned() { if (mFocusObject.isEmpty()) return false; MWWorld::Ptr ptr = MWMechanics::getPlayer(); MWWorld::Ptr victim; MWBase::MechanicsManager* mm = MWBase::Environment::get().getMechanicsManager(); return !mm->isAllowedToUse(ptr, mFocusObject, victim); } MyGUI::IntSize ToolTips::createToolTip(const MWGui::ToolTipInfo& info, bool isOwned) { mDynamicToolTipBox->setVisible(true); 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"); 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.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().getESMStore(); if (!info.enchant.empty()) { enchant = store.get().search(info.enchant); if (enchant) { if (enchant->mData.mType == ESM::Enchantment::CastOnce) text += "\n#{sItemCastOnce}"; else if (enchant->mData.mType == ESM::Enchantment::WhenStrikes) text += "\n#{sItemCastWhenStrikes}"; else if (enchant->mData.mType == ESM::Enchantment::WhenUsed) text += "\n#{sItemCastWhenUsed}"; else if (enchant->mData.mType == ESM::Enchantment::ConstantEffect) text += "\n#{sItemCastConstant}"; } } // this the maximum width of the tooltip before it starts word-wrapping setCoord(0, 0, 300, 300); const MyGUI::IntPoint padding(8, 8); const int imageCaptionHPadding = !caption.empty() ? 8 : 0; const int imageCaptionVPadding = !caption.empty() ? 4 : 0; const int maximumWidth = MyGUI::RenderManager::getInstance().getViewSize().width - imageCaptionHPadding * 2; 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"); captionWidget->setEditStatic(true); captionWidget->setNeedKeyFocus(false); captionWidget->setCaptionWithReplacing(caption); MyGUI::IntSize captionSize = captionWidget->getTextSize(); 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"); textWidget->setEditStatic(true); textWidget->setEditMultiLine(true); textWidget->setEditWordWrap(info.wordWrap); textWidget->setCaptionWithReplacing(text); textWidget->setTextAlign(MyGUI::Align::HCenter | MyGUI::Align::Top); textWidget->setNeedKeyFocus(false); 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.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); 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); 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); } if (!info.effects.empty()) { 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); effectsWidget->setEffectList(info.effects); std::vector effectItems; int flag = info.isPotion ? Widgets::MWEffectList::EF_NoTarget : 0; flag |= info.isIngredient ? Widgets::MWEffectList::EF_NoMagnitude : 0; 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::Align::Stretch); MyGUI::IntCoord coord(0, 6, totalSize.width, 24); 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, 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) { 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"); chargeText->setCaptionWithReplacing("#{sCharges}"); const int chargeTextWidth = chargeText->getTextSize().width + 5; const int chargeAndTextWidth = chargeWidth + chargeTextWidth; totalSize.width = std::max(totalSize.width, chargeAndTextWidth); 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); } else { chargeCoord = MyGUI::IntCoord( (totalSize.width - chargeAndTextWidth) / 2 + chargeTextWidth, coord.top + 6, chargeWidth, 18); } Widgets::MWDynamicStatPtr chargeWidget = enchantArea->createWidget( "MW_ChargeBar", chargeCoord, MyGUI::Align::Default); chargeWidget->setValue(charge, maxCharge); totalSize.height += 24; } } 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 (captionSize.width > maximumWidth) { mHorizontalScrollIndex = mHorizontalScrollIndex + 2; if (mHorizontalScrollIndex > captionSize.width) { mHorizontalScrollIndex = -totalSize.width; } int horizontal_scroll = mHorizontalScrollIndex; if (horizontal_scroll < 40) { horizontal_scroll = 40; } else { horizontal_scroll = 80 - mHorizontalScrollIndex; } 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 if (!image.empty()) { MyGUI::ImageBox* imageWidget = mDynamicToolTipBox->createWidget("ImageBox", 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); } totalSize += MyGUI::IntSize(padding.left * 2, padding.top * 2); return totalSize; } std::string ToolTips::toString(const float value) { std::ostringstream stream; if (value != int(value)) stream << std::setprecision(3); stream << value; return stream.str(); } std::string ToolTips::toString(const int value) { return std::to_string(value); } std::string ToolTips::getWeightString(const float weight, const std::string& prefix) { if (weight == 0) return {}; else return "\n" + prefix + ": " + toString(weight); } std::string ToolTips::getPercentString(const float value, const std::string& prefix) { if (value == 0) return {}; else return "\n" + prefix + ": " + toString(value * 100) + "%"; } std::string ToolTips::getValueString(const int value, const std::string& prefix) { if (value == 0) return {}; else return "\n" + prefix + ": " + toString(value); } std::string ToolTips::getMiscString(const std::string& text, const std::string& prefix) { if (text.empty()) return {}; else return "\n" + prefix + ": " + text; } std::string ToolTips::getCountString(const int value) { if (value == 1) return {}; else return " (" + MyGUI::utility::toString(value) + ")"; } std::string ToolTips::getSoulString(const MWWorld::CellRef& cellref) { const ESM::RefId& soul = cellref.getSoul(); if (soul.empty()) return {}; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Creature* creature = store.get().search(soul); if (!creature) return {}; if (creature->mName.empty()) return " (" + creature->mId.toDebugString() + ")"; return " (" + creature->mName + ")"; } std::string ToolTips::getCellRefString(const MWWorld::CellRef& cellref) { std::string ret; ret += getMiscString(cellref.getOwner().getRefIdString(), "Owner"); const ESM::RefId& factionId = cellref.getFaction(); if (!factionId.empty()) { 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.getRefIdString() : fact->mName, "Owner Faction"); if (cellref.getFactionRank() >= 0) { int rank = cellref.getFactionRank(); const std::string& rankName = fact->mRanks[rank]; if (rankName.empty()) ret += getValueString(cellref.getFactionRank(), "Rank"); else ret += getMiscString(rankName, "Rank"); } } } std::vector> itemOwners = MWBase::Environment::get().getMechanicsManager()->getStolenItemOwners(cellref.getRefId()); for (std::pair& owner : itemOwners) { if (owner.second == std::numeric_limits::max()) 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.toDebugString(); } ret += getMiscString(cellref.getGlobalVariable(), "Global"); return ret; } 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 += l10n->formatMessage("DurationSecond", { "seconds" }, { 0 }); return ret; } constexpr int secondsPerMinute = 60; // 60 seconds constexpr int secondsPerHour = secondsPerMinute * 60; // 60 minutes constexpr int secondsPerDay = secondsPerHour * 24; // 24 hours constexpr int secondsPerMonth = secondsPerDay * 30; // 30 days constexpr int secondsPerYear = secondsPerDay * 365; int fullDuration = static_cast(duration); 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 hours = fullDuration % secondsPerDay / secondsPerHour; int minutes = fullDuration % secondsPerHour / secondsPerMinute; int seconds = fullDuration % secondsPerMinute; if (years) { units++; ret += l10n->formatMessage("DurationYear", { "years" }, { years }); } if (months) { units++; ret += l10n->formatMessage("DurationMonth", { "months" }, { months }); } if (units < 2 && days) { units++; ret += l10n->formatMessage("DurationDay", { "days" }, { days }); } if (units < 2 && hours) { units++; ret += l10n->formatMessage("DurationHour", { "hours" }, { hours }); } if (units >= 2) return ret; if (minutes) ret += l10n->formatMessage("DurationMinute", { "minutes" }, { minutes }); if (seconds) ret += l10n->formatMessage("DurationSecond", { "seconds" }, { seconds }); return ret; } bool ToolTips::toggleFullHelp() { mFullHelp = !mFullHelp; return mFullHelp; } bool ToolTips::getFullHelp() const { return mFullHelp; } void ToolTips::setFocusObjectScreenCoords(float x, float y) { mFocusToolTipX = x; mFocusToolTipY = y; } void ToolTips::createSkillToolTip(MyGUI::Widget* widget, ESM::RefId skillId) { if (skillId.empty()) return; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Skill* skill = store.get().find(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", MyGUI::TextIterator::toTagsString(skill->mName)); widget->setUserString("Caption_SkillNoProgressDescription", skill->mDescription); widget->setUserString("Caption_SkillNoProgressAttribute", "#{sGoverningAttribute}: " + MyGUI::TextIterator::toTagsString(attr->mName)); widget->setUserString("ImageTexture_SkillNoProgressImage", skill->mIcon); } void ToolTips::createAttributeToolTip(MyGUI::Widget* widget, ESM::RefId attributeId) { const ESM::Attribute* attribute = MWBase::Environment::get().getESMStore()->get().search(attributeId); if (!attribute) return; widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "AttributeToolTip"); 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) { widget->setUserString("Caption_Caption", name); std::string specText; // get all skills of this specialisation const MWWorld::Store& skills = MWBase::Environment::get().getESMStore()->get(); bool isFirst = true; for (const auto& skill : skills) { if (skill.mData.mSpecialization == specId) { if (isFirst) isFirst = false; else specText += "\n"; specText += MyGUI::TextIterator::toTagsString(skill.mName); } } widget->setUserString("Caption_ColumnText", specText); widget->setUserString("ToolTipLayout", "SpecializationToolTip"); widget->setUserString("ToolTipType", "Layout"); } void ToolTips::createBirthsignToolTip(MyGUI::Widget* widget, const ESM::RefId& birthsignId) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); 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", Misc::ResourceHelpers::correctTexturePath(sign->mTexture, vfs)); std::string text = sign->mName + "\n#{fontcolourhtml=normal}" + sign->mDescription; std::vector abilities, powers, spells; for (const ESM::RefId& spellId : sign->mPowers.mList) { 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); if (type != ESM::Spell::ST_Spell && type != ESM::Spell::ST_Ability && type != ESM::Spell::ST_Power) continue; // We only want spell, ability and powers. if (type == ESM::Spell::ST_Ability) abilities.push_back(spell); else if (type == ESM::Spell::ST_Power) powers.push_back(spell); else if (type == ESM::Spell::ST_Spell) spells.push_back(spell); } using Category = std::pair&, std::string_view>; for (const auto& [category, label] : std::initializer_list{ { abilities, "sBirthsignmenu1" }, { powers, "sPowers" }, { spells, "sBirthsignmenu2" } }) { bool addHeader = true; for (const ESM::Spell* spell : category) { if (addHeader) { text += "\n\n#{fontcolourhtml=header}#{"; text += label; text += '}'; addHeader = false; } text += "\n#{fontcolourhtml=normal}" + spell->mName; } } widget->setUserString("Caption_BirthSignText", text); } void ToolTips::createRaceToolTip(MyGUI::Widget* widget, const ESM::Race* playerRace) { widget->setUserString("Caption_CenteredCaption", playerRace->mName); widget->setUserString("Caption_CenteredCaptionText", playerRace->mDescription); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "RaceToolTip"); } void ToolTips::createClassToolTip(MyGUI::Widget* widget, const ESM::Class& playerClass) { if (playerClass.mName.empty()) return; int spec = playerClass.mData.mSpecialization; std::string specStr = "#{"; specStr += ESM::Class::sGmstSpecializationIds[spec]; specStr += '}'; widget->setUserString("Caption_ClassName", playerClass.mName); widget->setUserString("Caption_ClassDescription", playerClass.mDescription); widget->setUserString("Caption_ClassSpecialisation", "#{sSpecialization}: " + specStr); widget->setUserString("ToolTipType", "Layout"); widget->setUserString("ToolTipLayout", "ClassToolTip"); } void ToolTips::createMagicEffectToolTip(MyGUI::Widget* widget, short 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 = 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}: " + MyGUI::TextIterator::toTagsString( store->get().find(effect->mData.mSchool)->mSchool->mName)); widget->setUserString("ImageTexture_MagicEffectImage", icon); } } openmw-openmw-0.49.0/apps/openmw/mwgui/tooltips.hpp000066400000000000000000000113101503074453300224020ustar00rootroot00000000000000#ifndef MWGUI_TOOLTIPS_H #define MWGUI_TOOLTIPS_H #include "../mwworld/ptr.hpp" #include "layout.hpp" #include "widgets.hpp" namespace ESM { struct Class; struct Race; } namespace MWGui { // Info about tooltip that is supplied by the MWWorld::Class object struct ToolTipInfo { public: ToolTipInfo() : imageSize(32) , remainingEnchantCharge(-1) , 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) ESM::RefId enchant; int remainingEnchantCharge; // effects (for potions, ingredients) Widgets::SpellEffectList effects; // local map notes std::vector notes; bool isPotion; // potions do not show target in the tooltip bool isIngredient; // ingredients have no effect magnitude bool wordWrap; }; class ToolTips : public Layout { public: ToolTips(); void onFrame(float frameDuration); void update(float frameDuration); void setEnabled(bool enabled); bool toggleFullHelp(); ///< show extra info in item tooltips (owner, script) bool getFullHelp() const; void clear(); void setFocusObject(const MWWorld::Ptr& focus); void setFocusObjectScreenCoords(float x, float y); ///< set the screen-space position of the tooltip for focused object static std::string getWeightString(const float weight, const std::string& prefix); static std::string getPercentString(const float value, const std::string& prefix); static std::string getValueString(const int value, const std::string& prefix); ///< @return "prefix: value" or "" if value is 0 static std::string getMiscString(const std::string& text, const std::string& prefix); ///< @return "prefix: text" or "" if text is empty static std::string toString(const float value); static std::string toString(const int value); static std::string getCountString(const int value); ///< @return blank string if count is 1, or else " (value)" static std::string getSoulString(const MWWorld::CellRef& cellref); ///< Returns a string containing the name of the creature that the ID in the cellref's soul field belongs to. 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); ///< 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, 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 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); ///< @return requested tooltip size MyGUI::IntSize createToolTip(const ToolTipInfo& info, bool isOwned = false); ///< @return requested tooltip size /// @param isFocusObject Is the object this tooltips originates from mFocusObject? float mFocusToolTipX; float mFocusToolTipY; /// 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); int mHorizontalScrollIndex; float mRemainingDelay; // remaining time until tooltip will show int mLastMouseX; int mLastMouseY; bool mEnabled; bool mFullHelp; float mFrameDuration; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/tradeitemmodel.cpp000066400000000000000000000144601503074453300235300ustar00rootroot00000000000000#include "tradeitemmodel.hpp" #include #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" namespace MWGui { TradeItemModel::TradeItemModel(std::unique_ptr sourceModel, const MWWorld::Ptr& merchant) : mMerchant(merchant) { mSourceModel = std::move(sourceModel); } bool TradeItemModel::allowedToUseItems() const { return true; } ItemStack TradeItemModel::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 TradeItemModel::getItemCount() { return mItems.size(); } void TradeItemModel::borrowImpl(const ItemStack& item, std::vector& out) { bool found = false; for (ItemStack& itemStack : out) { if (itemStack.mBase == item.mBase) { itemStack.mCount += item.mCount; found = true; break; } } if (!found) out.push_back(item); } void TradeItemModel::unborrowImpl(const ItemStack& item, size_t count, std::vector& out) { std::vector::iterator it = out.begin(); bool found = false; for (; it != out.end(); ++it) { if (it->mBase == item.mBase) { if (it->mCount < count) throw std::runtime_error("Not enough borrowed items to return"); it->mCount -= count; if (it->mCount == 0) out.erase(it); found = true; break; } } if (!found) throw std::runtime_error("Can't find borrowed item to return"); } 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) { ItemStack item = source->getItem(itemIndex); item.mCount = count; borrowImpl(item, mBorrowedToUs); } 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) { ItemStack item = source->getItem(itemIndex); unborrowImpl(item, count, mBorrowedFromUs); } void TradeItemModel::adjustEncumbrance(float& encumbrance) { for (ItemStack& itemStack : mBorrowedToUs) { MWWorld::Ptr& item = itemStack.mBase; encumbrance += item.getClass().getWeight(item) * itemStack.mCount; } for (ItemStack& itemStack : mBorrowedFromUs) { MWWorld::Ptr& item = itemStack.mBase; encumbrance -= item.getClass().getWeight(item) * itemStack.mCount; } encumbrance = std::max(0.f, encumbrance); } void TradeItemModel::abort() { mBorrowedFromUs.clear(); mBorrowedToUs.clear(); } const std::vector TradeItemModel::getItemsBorrowedToUs() const { return mBorrowedToUs; } void TradeItemModel::transferItems() { for (ItemStack& itemStack : mBorrowedToUs) { // get index in the source model ItemModel* sourceModel = itemStack.mCreator; size_t i = 0; for (; i < sourceModel->getItemCount(); ++i) { if (itemStack.mBase == sourceModel->getItem(i).mBase) break; } if (i == sourceModel->getItemCount()) throw std::runtime_error("The borrowed item disappeared"); sourceModel->moveItem( sourceModel->getItem(i), itemStack.mCount, this, !Settings::game().mPreventMerchantEquipping); } mBorrowedToUs.clear(); mBorrowedFromUs.clear(); } void TradeItemModel::update() { mSourceModel->update(); int services = 0; if (!mMerchant.isEmpty()) services = mMerchant.getClass().getServices(mMerchant); mItems.clear(); // add regular items for (size_t i = 0; i < mSourceModel->getItemCount(); ++i) { ItemStack item = mSourceModel->getItem(i); if (!mMerchant.isEmpty()) { MWWorld::Ptr base = item.mBase; if (base.getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) continue; if (!base.getClass().showsInInventory(base)) return; if (!base.getClass().canSell(base, services)) continue; // Bound items may not be bought if (item.mFlags & ItemStack::Flag_Bound) continue; // don't show equipped items if (mMerchant.getClass().hasInventoryStore(mMerchant)) { MWWorld::InventoryStore& store = mMerchant.getClass().getInventoryStore(mMerchant); if (store.isEquipped(base)) continue; } } // don't show items that we borrowed to someone else for (ItemStack& itemStack : mBorrowedFromUs) { if (itemStack.mBase == item.mBase) { if (item.mCount < itemStack.mCount) throw std::runtime_error("Lent more items than present"); item.mCount -= itemStack.mCount; } } if (item.mCount > 0) mItems.push_back(item); } // add items borrowed to us for (ItemStack& itemStack : mBorrowedToUs) { itemStack.mType = ItemStack::Type_Barter; mItems.push_back(itemStack); } } } openmw-openmw-0.49.0/apps/openmw/mwgui/tradeitemmodel.hpp000066400000000000000000000035131503074453300235320ustar00rootroot00000000000000#ifndef MWGUI_TRADE_ITEM_MODEL_H #define MWGUI_TRADE_ITEM_MODEL_H #include "itemmodel.hpp" namespace MWGui { class ItemModel; /// @brief An item model that allows 'borrowing' items from another item model. Used for previewing barter offers. /// Also filters items that the merchant does not sell. class TradeItemModel : public ProxyItemModel { public: TradeItemModel(std::unique_ptr sourceModel, const MWWorld::Ptr& merchant); bool allowedToUseItems() const override; ItemStack getItem(ModelIndex index) override; size_t getItemCount() override; void update() override; void borrowItemFromUs(ModelIndex itemIndex, 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 returnItemBorrowedFromUs(ModelIndex itemIndex, ItemModel* source, size_t count); /// Permanently transfers items that were borrowed to us from another model to this model 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); const std::vector getItemsBorrowedToUs() const; private: void borrowImpl(const ItemStack& item, std::vector& out); void unborrowImpl(const ItemStack& item, size_t count, std::vector& out); std::vector mItems; std::vector mBorrowedToUs; std::vector mBorrowedFromUs; MWWorld::Ptr mMerchant; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/tradewindow.cpp000066400000000000000000000614751503074453300230700ustar00rootroot00000000000000#include "tradewindow.hpp" #include #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.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/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "containeritemmodel.hpp" #include "countdialog.hpp" #include "inventorywindow.hpp" #include "itemview.hpp" #include "sortfilteritemmodel.hpp" #include "tooltips.hpp" #include "tradeitemmodel.hpp" namespace { int getEffectiveValue(MWWorld::Ptr item, int count) { float price = static_cast(item.getClass().getValue(item)); if (item.getClass().hasItemHealth(item)) { price *= item.getClass().getItemNormalizedHealth(item); } 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 { TradeWindow::TradeWindow() : WindowBase("openmw_trade_window.layout") , mSortModel(nullptr) , mTradeModel(nullptr) , mItemToSell(-1) , mCurrentBalance(0) , mCurrentMerchantOffer(0) { getWidget(mFilterAll, "AllButton"); getWidget(mFilterWeapon, "WeaponButton"); getWidget(mFilterApparel, "ApparelButton"); getWidget(mFilterMagic, "MagicButton"); getWidget(mFilterMisc, "MiscButton"); getWidget(mMaxSaleButton, "MaxSaleButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mOfferButton, "OfferButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mMerchantGold, "MerchantGold"); getWidget(mIncreaseButton, "IncreaseButton"); getWidget(mDecreaseButton, "DecreaseButton"); getWidget(mTotalBalance, "TotalBalance"); getWidget(mTotalBalanceLabel, "TotalBalanceLabel"); getWidget(mBottomPane, "BottomPane"); getWidget(mFilterEdit, "FilterEdit"); getWidget(mItemView, "ItemView"); mItemView->eventItemClicked += MyGUI::newDelegate(this, &TradeWindow::onItemSelected); mFilterAll->setStateSelected(true); mFilterAll->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterWeapon->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterApparel->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMagic->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterMisc->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onFilterChanged); mFilterEdit->eventEditTextChange += MyGUI::newDelegate(this, &TradeWindow::onNameFilterChanged); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onCancelButtonClicked); mOfferButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onOfferButtonClicked); mMaxSaleButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TradeWindow::onMaxSaleButtonClicked); mIncreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onIncreaseButtonPressed); mIncreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); mDecreaseButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &TradeWindow::onDecreaseButtonPressed); mDecreaseButton->eventMouseButtonReleased += MyGUI::newDelegate(this, &TradeWindow::onBalanceButtonReleased); 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 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; mCurrentMerchantOffer = 0; std::vector itemSources; // Important: actor goes first, so purchased items come out of the actor's pocket first itemSources.push_back(actor); MWBase::Environment::get().getWorld()->getContainersOwnedBy(actor, itemSources); std::vector worldItems; MWBase::Environment::get().getWorld()->getItemsOwnedBy(actor, worldItems); 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(); setTitle(actor.getClass().getName(actor)); onFilterChanged(mFilterAll); mFilterEdit->setCaption({}); } void TradeWindow::onFrame(float dt) { checkReferenceAvailable(); } void TradeWindow::onNameFilterChanged(MyGUI::EditBox* _sender) { mSortModel->setNameFilter(_sender->getCaption()); mItemView->update(); } void TradeWindow::onFilterChanged(MyGUI::Widget* _sender) { if (_sender == mFilterAll) mSortModel->setCategory(SortFilterItemModel::Category_All); else if (_sender == mFilterWeapon) mSortModel->setCategory(SortFilterItemModel::Category_Weapon); else if (_sender == mFilterApparel) mSortModel->setCategory(SortFilterItemModel::Category_Apparel); else if (_sender == mFilterMagic) mSortModel->setCategory(SortFilterItemModel::Category_Magic); else if (_sender == mFilterMisc) mSortModel->setCategory(SortFilterItemModel::Category_Misc); mFilterAll->setStateSelected(false); mFilterWeapon->setStateSelected(false); mFilterApparel->setStateSelected(false); mFilterMagic->setStateSelected(false); mFilterMisc->setStateSelected(false); _sender->castType()->setStateSelected(true); mItemView->update(); } int TradeWindow::getMerchantServices() { return mPtr.getClass().getServices(mPtr); } bool TradeWindow::exit() { mTradeModel->abort(); MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel()->abort(); return true; } void TradeWindow::onItemSelected(int index) { const ItemStack& item = mSortModel->getItem(index); MWWorld::Ptr object = item.mBase; int count = item.mCount; bool shift = MyGUI::InputManager::getInstance().isShiftPressed(); if (MyGUI::InputManager::getInstance().isControlPressed()) count = 1; if (count > 1 && !shift) { CountDialog* dialog = MWBase::Environment::get().getWindowManager()->getCountDialog(); std::string message = "#{sQuanityMenuMessage02}"; 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); mItemToSell = mSortModel->mapToSource(index); } else { mItemToSell = mSortModel->mapToSource(index); sellItem(nullptr, count); } } void TradeWindow::sellItem(MyGUI::Widget* sender, int count) { const ItemStack& item = mTradeModel->getItem(mItemToSell); const ESM::RefId& sound = item.mBase.getClass().getUpSoundId(item.mBase); MWBase::Environment::get().getWindowManager()->playSound(sound); TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); if (item.mType == ItemStack::Type_Barter) { // this was an item borrowed to us by the player mTradeModel->returnItemBorrowedToUs(mItemToSell, count); playerTradeModel->returnItemBorrowedFromUs(mItemToSell, mTradeModel, count); buyFromNpc(item.mBase, count, true); } else { // borrow item to player playerTradeModel->borrowItemToUs(mItemToSell, mTradeModel, count); mTradeModel->borrowItemFromUs(mItemToSell, count); buyFromNpc(item.mBase, count, false); } MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); mItemView->update(); } void TradeWindow::borrowItem(int index, size_t count) { 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) { TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); const ItemStack& item = playerTradeModel->getItem(index); mTradeModel->returnItemBorrowedFromUs(index, playerTradeModel, count); mItemView->update(); sellToNpc(item.mBase, count, true); } void TradeWindow::addOrRemoveGold(int amount, const MWWorld::Ptr& actor) { MWWorld::ContainerStore& store = actor.getClass().getContainerStore(actor); if (amount > 0) { store.add(MWWorld::ContainerStore::sGoldId, amount); } else { store.remove(MWWorld::ContainerStore::sGoldId, -amount); } } void TradeWindow::onOfferButtonClicked(MyGUI::Widget* _sender) { TradeItemModel* playerItemModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); 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(); const std::vector& merchantBought = mTradeModel->getItemsBorrowedToUs(); if (playerBought.empty() && merchantBought.empty()) { // user notification MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog11}"); return; } MWWorld::Ptr player = MWMechanics::getPlayer(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); // check if the player can afford this if (mCurrentBalance < 0 && playerGold < std::abs(mCurrentBalance)) { // user notification MWBase::Environment::get().getWindowManager()->messageBox("#{sBarterDialog1}"); return; } // check if the merchant can afford this if (mCurrentBalance > 0 && getMerchantGold() < mCurrentBalance) { // user notification 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)) { 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); onCancelButtonClicked(mCancelButton); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return; } } 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(); MWBase::Environment::get().getDialogueManager()->applyBarterDispositionChange(dispositionDelta); } // display message on haggle failure if (!offerAccepted) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage9}"); return; } // make the item transfer mTradeModel->transferItems(); playerItemModel->transferItems(); // transfer the gold if (mCurrentBalance != 0) { addOrRemoveGold(mCurrentBalance, player); mPtr.getClass().getCreatureStats(mPtr).setGoldPool( mPtr.getClass().getCreatureStats(mPtr).getGoldPool() - mCurrentBalance); } eventTradeDone(); MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Gold Up")); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } void TradeWindow::onAccept(MyGUI::EditBox* sender) { onOfferButtonClicked(sender); // To do not spam onAccept() again and again MWBase::Environment::get().getWindowManager()->injectKeyRelease(MyGUI::KeyCode::None); } void TradeWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { exit(); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } void TradeWindow::onMaxSaleButtonClicked(MyGUI::Widget* _sender) { mCurrentBalance = getMerchantGold(); updateLabels(); } void TradeWindow::addRepeatController(MyGUI::Widget* widget) { 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); } void TradeWindow::onIncreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onIncreaseButtonTriggered(); } void TradeWindow::onDecreaseButtonPressed(MyGUI::Widget* _sender, int _left, int _top, MyGUI::MouseButton _id) { addRepeatController(_sender); onDecreaseButtonTriggered(); } void TradeWindow::onRepeatClick(MyGUI::Widget* widget, MyGUI::ControllerItem* controller) { if (widget == mIncreaseButton) onIncreaseButtonTriggered(); else if (widget == mDecreaseButton) onDecreaseButtonTriggered(); } 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)); } 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) return; if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance -= 1; else mCurrentBalance += 1; updateLabels(); } void TradeWindow::onDecreaseButtonTriggered() { if (mTotalBalance->getValue() == 0) mCurrentBalance = 0; if (mCurrentBalance < 0) mCurrentBalance += 1; else mCurrentBalance -= 1; updateLabels(); } void TradeWindow::updateLabels() { 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}"); } else { mTotalBalanceLabel->setCaptionWithReplacing("#{sTotalSold}"); } mTotalBalance->setValue(std::abs(mCurrentBalance)); mMerchantGold->setCaptionWithReplacing("#{sSellerGold} " + MyGUI::utility::toString(getMerchantGold())); } void TradeWindow::updateOffer() { TradeItemModel* playerTradeModel = MWBase::Environment::get().getWindowManager()->getInventoryWindow()->getTradeModel(); int merchantOffer = 0; // The offered price must be capped at 75% of the base price to avoid exploits // connected to buying and selling the same item. // This value has been determined by researching the limitations of the vanilla formula // and may not be sufficient if getBarterOffer behavior has been changed. const std::vector& playerBorrowed = playerTradeModel->getItemsBorrowedToUs(); 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); merchantOffer -= std::max(cap, buyingPrice); } const std::vector& merchantBorrowed = mTradeModel->getItemsBorrowedToUs(); 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); merchantOffer += mPtr.getClass().isNpc() ? std::min(cap, sellingPrice) : sellingPrice; } int diff = merchantOffer - mCurrentMerchantOffer; mCurrentMerchantOffer = merchantOffer; mCurrentBalance += diff; updateLabels(); } void TradeWindow::sellToNpc(const MWWorld::Ptr& item, int count, bool boughtItem) { updateOffer(); } void TradeWindow::buyFromNpc(const MWWorld::Ptr& item, int count, bool soldItem) { updateOffer(); } void TradeWindow::onReferenceUnavailable() { // 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(); } int TradeWindow::getMerchantGold() { int merchantGold = mPtr.getClass().getCreatureStats(mPtr).getGoldPool(); return merchantGold; } void TradeWindow::resetReference() { ReferenceInterface::resetReference(); mItemView->setModel(nullptr); mTradeModel = nullptr; mSortModel = nullptr; } void TradeWindow::onClose() { // Make sure the window was actually closed and not temporarily hidden. if (MWBase::Environment::get().getWindowManager()->containsMode(GM_Barter)) return; resetReference(); } void TradeWindow::onDeleteCustomData(const MWWorld::Ptr& ptr) { if (mTradeModel && mTradeModel->usesContainer(ptr)) MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Barter); } } openmw-openmw-0.49.0/apps/openmw/mwgui/tradewindow.hpp000066400000000000000000000067761503074453300231000ustar00rootroot00000000000000#ifndef MWGUI_TRADEWINDOW_H #define MWGUI_TRADEWINDOW_H #include "referenceinterface.hpp" #include "windowbase.hpp" namespace Gui { class NumericEditBox; } namespace MyGUI { class ControllerItem; } namespace MWGui { class ItemView; class SortFilterItemModel; class TradeItemModel; class TradeWindow : public WindowBase, public ReferenceInterface { public: TradeWindow(); void setPtr(const MWWorld::Ptr& actor) override; void onClose() override; void onFrame(float dt) override; void clear() override { resetReference(); } void borrowItem(int index, size_t count); void returnItem(int index, size_t count); int getMerchantServices(); bool exit() override; void resetReference() override; void onDeleteCustomData(const MWWorld::Ptr& ptr) override; typedef MyGUI::delegates::MultiDelegate<> EventHandle_TradeDone; EventHandle_TradeDone eventTradeDone; std::string_view getWindowIdForLua() const override { return "Trade"; } private: ItemView* mItemView; SortFilterItemModel* mSortModel; TradeItemModel* mTradeModel; 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::EditBox* mFilterEdit; MyGUI::Button* mIncreaseButton; MyGUI::Button* mDecreaseButton; MyGUI::TextBox* mTotalBalanceLabel; Gui::NumericEditBox* mTotalBalance; MyGUI::Widget* mBottomPane; MyGUI::Button* mMaxSaleButton; MyGUI::Button* mCancelButton; MyGUI::Button* mOfferButton; MyGUI::TextBox* mPlayerGold; MyGUI::TextBox* mMerchantGold; int mItemToSell; 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 updateOffer(); 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 addRepeatController(MyGUI::Widget* widget); void onIncreaseButtonTriggered(); void onDecreaseButtonTriggered(); void addOrRemoveGold(int gold, const MWWorld::Ptr& actor); void updateLabels(); void onReferenceUnavailable() override; int getMerchantGold(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/trainingwindow.cpp000066400000000000000000000207061503074453300235740ustar00rootroot00000000000000#include "trainingwindow.hpp" #include #include #include #include "../mwbase/environment.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/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include #include #include "tooltips.hpp" namespace MWGui { TrainingWindow::TrainingWindow() : WindowBase("openmw_trainingwindow.layout") , mTimeAdvancer(0.05f) { getWidget(mTrainingOptions, "TrainingOptions"); getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onCancelButtonClicked); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &TrainingWindow::onTrainingProgressChanged); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &TrainingWindow::onTrainingFinished); } void TrainingWindow::onOpen() { if (mTimeAdvancer.isRunning()) { mProgressBar.setVisible(true); setVisible(false); } else mProgressBar.setVisible(false); center(); } 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(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); mPlayerGold->setCaptionWithReplacing("#{sGold}: " + MyGUI::utility::toString(playerGold)); const auto& store = MWBase::Environment::get().getESMStore(); const MWWorld::Store& 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, 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); } } } MyGUI::EnumeratorWidgetPtr widgets = mTrainingOptions->getEnumerator(); MyGUI::Gui::getInstance().destroyWidgets(widgets); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); const int lineHeight = Settings::gui().mFontSize + 2; for (size_t i = 0; i < skills.size(); ++i) { 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 * lineHeight, mTrainingOptions->getWidth() - 10, lineHeight), MyGUI::Align::Default); button->setUserData(skills[i].first); button->eventMouseButtonClick += MyGUI::newDelegate(this, &TrainingWindow::onTrainingSelected); button->setCaptionWithReplacing( MyGUI::TextIterator::toTagsString(skill->mName) + " - " + MyGUI::utility::toString(price)); button->setSize(button->getTextSize().width + 12, button->getSize().height); ToolTips::createSkillToolTip(button, skill->mId); } center(); } void TrainingWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } void TrainingWindow::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Training); } void TrainingWindow::onTrainingSelected(MyGUI::Widget* sender) { const ESM::Skill* skill = *sender->getUserData(); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); MWMechanics::NpcStats& pcStats = player.getClass().getNpcStats(player); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); 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), skill->mId) <= pcStats.getSkill(skill->mId).getBase()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sServiceTrainingWords}"); return; } // You can not train a skill above its governing attribute if (pcStats.getSkill(skill->mId).getBase() >= pcStats.getAttribute(ESM::Attribute::indexToRefId(skill->mData.mAttribute)).getModified()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage17}"); return; } // increase skill MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill->mId, "trainer"); // remove gold 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); setVisible(false); mProgressBar.setVisible(true); mProgressBar.setProgress(0, 2); mTimeAdvancer.run(2); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2f); MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f, false, 0.2f); } void TrainingWindow::onTrainingProgressChanged(int cur, int total) { mProgressBar.setProgress(cur, total); } void TrainingWindow::onTrainingFinished() { 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()->exitCurrentGuiMode(); } float TrainingWindow::getSkillForTraining(const MWMechanics::NpcStats& stats, ESM::RefId id) const { if (Settings::game().mTrainersTrainingSkillsBasedOnBaseSkill) return stats.getSkill(id).getBase(); return stats.getSkill(id).getModified(); } void TrainingWindow::onFrame(float dt) { checkReferenceAvailable(); mTimeAdvancer.onFrame(dt); } bool TrainingWindow::exit() { return !mTimeAdvancer.isRunning(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/trainingwindow.hpp000066400000000000000000000026641503074453300236040ustar00rootroot00000000000000#ifndef MWGUI_TRAININGWINDOW_H #define MWGUI_TRAININGWINDOW_H #include "referenceinterface.hpp" #include "timeadvancer.hpp" #include "waitdialog.hpp" #include "windowbase.hpp" namespace MWMechanics { class NpcStats; } namespace MWGui { class TrainingWindow : public WindowBase, public ReferenceInterface { public: TrainingWindow(); void onOpen() override; bool exit() override; void setPtr(const MWWorld::Ptr& actor) override; void onFrame(float dt) override; WindowBase* getProgressBar() { return &mProgressBar; } void clear() override { resetReference(); } std::string_view getWindowIdForLua() const override { return "Training"; } protected: void onReferenceUnavailable() override; void onCancelButtonClicked(MyGUI::Widget* sender); void onTrainingSelected(MyGUI::Widget* sender); void onTrainingProgressChanged(int cur, int total); void onTrainingFinished(); // 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, ESM::RefId id) const; MyGUI::Widget* mTrainingOptions; MyGUI::Button* mCancelButton; MyGUI::TextBox* mPlayerGold; WaitDialogProgressBar mProgressBar; TimeAdvancer mTimeAdvancer; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/travelwindow.cpp000066400000000000000000000237121503074453300232560ustar00rootroot00000000000000#include "travelwindow.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/actionteleport.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.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") , mCurrentY(0) { getWidget(mCancelButton, "CancelButton"); getWidget(mPlayerGold, "PlayerGold"); getWidget(mSelect, "Select"); getWidget(mDestinations, "Travel"); getWidget(mDestinationsView, "DestinationsView"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onCancelButtonClicked); mDestinations->setCoord(450 / 2 - mDestinations->getTextSize().width / 2, mDestinations->getTop(), mDestinations->getTextSize().width, mDestinations->getHeight()); mSelect->setCoord(8, mSelect->getTop(), mSelect->getTextSize().width, mSelect->getHeight()); } void TravelWindow::addDestination(const ESM::RefId& name, const ESM::Position& pos, bool interior) { int price; const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); int playerGold = player.getClass().getContainerStore(player).count(MWWorld::ContainerStore::sGoldId); if (!mPtr.getCell()->isExterior()) { price = gmst.find("fMagesGuildTravel")->mValue.getInteger(); } 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 fTravelMult = gmst.find("fTravelMult")->mValue.getFloat(); if (fTravelMult != 0) price = static_cast(d / fTravelMult); else price = static_cast(d); } price = std::max(1, price); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); // Add price for the travelling followers std::set followers; MWWorld::ActionTeleport::getFollowers(player, followers, !interior); // Apply followers cost, unlike vanilla the first follower doesn't travel for free price *= 1 + static_cast(followers.size()); const int lineHeight = Settings::gui().mFontSize + 2; 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"); else toAdd->setUserString("interior", "n"); const std::string& nameString = name.getRefIdString(); toAdd->setUserString("price", std::to_string(price)); toAdd->setCaptionWithReplacing( "#{sCell=" + nameString + "} - " + MyGUI::utility::toString(price) + "#{sgp}"); toAdd->setSize(mDestinationsView->getWidth(), lineHeight); toAdd->eventMouseWheel += MyGUI::newDelegate(this, &TravelWindow::onMouseWheel); toAdd->setUserString("Destination", nameString); toAdd->setUserData(pos); toAdd->eventMouseButtonClick += MyGUI::newDelegate(this, &TravelWindow::onTravelButtonClick); } void TravelWindow::clearDestinations() { mDestinationsView->setViewOffset(MyGUI::IntPoint(0, 0)); mCurrentY = 0; while (mDestinationsView->getChildCount()) MyGUI::Gui::getInstance().destroyWidget(mDestinationsView->getChildAt(0)); } 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(); std::vector transport; if (mPtr.getClass().isNpc()) transport = mPtr.get()->mBase->getTransport(); else if (mPtr.getType() == ESM::Creature::sRecordId) transport = mPtr.get()->mBase->getTransport(); for (const auto& dest : transport) { std::string_view cellname = dest.mCellName; bool interior = true; const ESM::ExteriorCellLocation cellIndex = ESM::positionToExteriorCellLocation(dest.mPos.pos[0], dest.mPos.pos[1]); if (cellname.empty()) { MWWorld::CellStore& cell = MWBase::Environment::get().getWorldModel()->getExterior(cellIndex); cellname = MWBase::Environment::get().getWorld()->getCellName(&cell); interior = false; } 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 mDestinationsView->setVisibleVScroll(false); mDestinationsView->setCanvasSize( MyGUI::IntSize(mDestinationsView->getWidth(), std::max(mDestinationsView->getHeight(), mCurrentY))); mDestinationsView->setVisibleVScroll(true); } void TravelWindow::onTravelButtonClick(MyGUI::Widget* _sender) { 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 (playerGold < price) return; // Set "traveling" flag, so GetPCTraveling can detect teleportation. // We will reset this flag during next world update. MWBase::Environment::get().getWorld()->setPlayerTraveling(true); if (!mPtr.getCell()->isExterior()) // Interior cell -> mages guild transport MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("mysticism cast")); 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()->fadeScreenOut(1); ESM::Position pos = *_sender->getUserData(); 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() .getESMStore() ->get() .find("fTravelTimeMult") ->mValue.getFloat()); MWBase::Environment::get().getMechanicsManager()->rest(hours, true); MWBase::Environment::get().getWorld()->advanceTime(hours); } MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); 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(cellId, pos, true); action.execute(player); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } void TravelWindow::onCancelButtonClicked(MyGUI::Widget* _sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); } void TravelWindow::updateLabels() { 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()); } void TravelWindow::onReferenceUnavailable() { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Travel); MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); } void TravelWindow::onMouseWheel(MyGUI::Widget* _sender, int _rel) { 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))); } } openmw-openmw-0.49.0/apps/openmw/mwgui/travelwindow.hpp000066400000000000000000000020711503074453300232560ustar00rootroot00000000000000#ifndef MWGUI_TravelWINDOW_H #define MWGUI_TravelWINDOW_H #include "referenceinterface.hpp" #include "windowbase.hpp" namespace MyGUI { class Gui; class Widget; } namespace MWGui { class TravelWindow : public ReferenceInterface, public WindowBase { public: TravelWindow(); 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; MyGUI::ScrollView* mDestinationsView; 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 onReferenceUnavailable() override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/videowidget.cpp000066400000000000000000000054601503074453300230430ustar00rootroot00000000000000#include "videowidget.hpp" #include #include #include #include #include #include #include "../mwsound/movieaudiofactory.hpp" namespace MWGui { VideoWidget::VideoWidget() : mVFS(nullptr) { mPlayer = std::make_unique(); setNeedKeyFocus(true); } VideoWidget::~VideoWidget() = default; void VideoWidget::setVFS(const VFS::Manager* vfs) { mVFS = vfs; } void VideoWidget::playVideo(const std::string& video) { mPlayer->setAudioFactory(new MWSound::MovieAudioFactory()); Files::IStreamPtr videoStream; try { videoStream = mVFS->get(video); } catch (std::exception& e) { Log(Debug::Error) << "Failed to open video: " << e.what(); return; } mPlayer->playVideo(std::move(videoStream), video); osg::ref_ptr texture = mPlayer->getVideoTexture(); if (!texture) return; mTexture = std::make_unique(texture); setRenderItemTexture(mTexture.get()); getSubWidgetMain()->_setUVSet(MyGUI::FloatRect(0.f, 1.f, 1.f, 0.f)); } int VideoWidget::getVideoWidth() { return mPlayer->getVideoWidth(); } int VideoWidget::getVideoHeight() { return mPlayer->getVideoHeight(); } bool VideoWidget::update() { return mPlayer->update(); } void VideoWidget::stop() { mPlayer->close(); } void VideoWidget::pause() { mPlayer->pause(); } void VideoWidget::resume() { mPlayer->play(); } bool VideoWidget::isPaused() const { return mPlayer->isPaused(); } bool VideoWidget::hasAudioStream() { return mPlayer->hasAudioStream(); } void VideoWidget::autoResize(bool stretch) { 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); setCoord(leftPadding, topPadding, screenSize.width - leftPadding * 2, screenSize.height - topPadding * 2); } else setCoord(0, 0, screenSize.width, screenSize.height); } } openmw-openmw-0.49.0/apps/openmw/mwgui/videowidget.hpp000066400000000000000000000027551503074453300230540ustar00rootroot00000000000000#ifndef OPENMW_MWGUI_VIDEOWIDGET_H #define OPENMW_MWGUI_VIDEOWIDGET_H #include #include namespace Video { class VideoPlayer; } namespace VFS { class Manager; } namespace MWGui { /** * Widget that plays a video. */ class VideoWidget : public MyGUI::Widget { public: 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); int getVideoWidth(); int getVideoHeight(); /// @return Is the video still playing? bool update(); /// Return true if a video is currently playing and it has an audio stream. bool hasAudioStream(); /// Stop video and free resources (done automatically on destruction) void stop(); void pause(); void resume(); bool isPaused() const; /// Adjust the coordinates of this video widget relative to its parent, /// 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); private: const VFS::Manager* mVFS; std::unique_ptr mTexture; std::unique_ptr mPlayer; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/waitdialog.cpp000066400000000000000000000313121503074453300226500ustar00rootroot00000000000000#include "waitdialog.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.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" namespace MWGui { WaitDialogProgressBar::WaitDialogProgressBar() : WindowBase("openmw_wait_dialog_progressbar.layout") { getWidget(mProgressBar, "ProgressBar"); getWidget(mProgressText, "ProgressText"); } void WaitDialogProgressBar::onOpen() { center(); } void WaitDialogProgressBar::setProgress(int cur, int total) { mProgressBar->setProgressRange(total); mProgressBar->setProgressPosition(cur); mProgressText->setCaption(MyGUI::utility::toString(cur) + "/" + MyGUI::utility::toString(total)); } // --------------------------------------------------------------------------------------------------------- WaitDialog::WaitDialog() : WindowBase("openmw_wait_dialog.layout") , mTimeAdvancer(0.05f) , mSleeping(false) , mHours(1) , mManualHours(1) , mFadeTimeRemaining(0) , mInterruptAt(-1) , mProgressBar() { getWidget(mDateTimeText, "DateTimeText"); getWidget(mRestText, "RestText"); getWidget(mHourText, "HourText"); getWidget(mUntilHealedButton, "UntilHealedButton"); getWidget(mWaitButton, "WaitButton"); getWidget(mCancelButton, "CancelButton"); getWidget(mHourSlider, "HourSlider"); mCancelButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onCancelButtonClicked); mUntilHealedButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onUntilHealedButtonClicked); mWaitButton->eventMouseButtonClick += MyGUI::newDelegate(this, &WaitDialog::onWaitButtonClicked); mHourSlider->eventScrollChangePosition += MyGUI::newDelegate(this, &WaitDialog::onHourSliderChangedPosition); mCancelButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mWaitButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mUntilHealedButton->eventKeyButtonPressed += MyGUI::newDelegate(this, &WaitDialog::onKeyButtonPressed); mTimeAdvancer.eventProgressChanged += MyGUI::newDelegate(this, &WaitDialog::onWaitingProgressChanged); mTimeAdvancer.eventInterrupted += MyGUI::newDelegate(this, &WaitDialog::onWaitingInterrupted); mTimeAdvancer.eventFinished += MyGUI::newDelegate(this, &WaitDialog::onWaitingFinished); } void WaitDialog::setPtr(const MWWorld::Ptr& ptr) { setCanRest(!ptr.isEmpty() || MWBase::Environment::get().getWorld()->canRest() == MWBase::World::Rest_Allowed); 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()->removeGuiMode(GM_Rest); } if (mUntilHealedButton->getVisible()) MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mUntilHealedButton); else MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mWaitButton); } bool WaitDialog::exit() { 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(); } void WaitDialog::onOpen() { if (mTimeAdvancer.isRunning()) { mProgressBar.setVisible(true); setVisible(false); return; } else { mProgressBar.setVisible(false); } if (!MWBase::Environment::get().getWindowManager()->getRestEnabled()) { MWBase::Environment::get().getWindowManager()->popGuiMode(); } MWBase::World::RestPermitted canRest = MWBase::Environment::get().getWorld()->canRest(); if (canRest == MWBase::World::Rest_EnemiesAreNearby) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); MWBase::Environment::get().getWindowManager()->popGuiMode(); } else if (canRest == MWBase::World::Rest_PlayerIsUnderwater) { // resting underwater not allowed MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage1}"); MWBase::Environment::get().getWindowManager()->popGuiMode(); } onHourSliderChangedPosition(mHourSlider, 0); mHourSlider->setScrollPosition(0); 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 = 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) { int autoHours = MWBase::Environment::get().getMechanicsManager()->getHoursToRest(); startWaiting(autoHours); } void WaitDialog::onWaitButtonClicked(MyGUI::Widget* sender) { startWaiting(mManualHours); } void WaitDialog::startWaiting(int hoursToWait) { if (Settings::saves().mAutosave) // autosaves when enabled MWBase::Environment::get().getStateManager()->quickSave("Autosave"); MWBase::World* world = MWBase::Environment::get().getWorld(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0.2f); mFadeTimeRemaining = 0.4f; setVisible(false); mHours = hoursToWait; // FIXME: move this somewhere else? mInterruptAt = -1; MWWorld::Ptr player = world->getPlayerPtr(); if (mSleeping && player.getCell()->isExterior()) { const ESM::RefId& regionstr = player.getCell()->getCell()->getRegion(); if (!regionstr.empty()) { 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, world->getPrng()); float fSleepRandMod = world->getStore().get().find("fSleepRandMod")->mValue.getFloat(); if (x < fSleepRandMod * hoursToWait) { float fSleepRestMod = world->getStore().get().find("fSleepRestMod")->mValue.getFloat(); int interruptAtHoursRemaining = int(fSleepRestMod * hoursToWait); if (interruptAtHoursRemaining != 0) { mInterruptAt = hoursToWait - interruptAtHoursRemaining; mInterruptCreatureList = region->mSleepList; } } } } } mProgressBar.setProgress(0, hoursToWait); } void WaitDialog::onCancelButtonClicked(MyGUI::Widget* sender) { MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); } void WaitDialog::onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position) { 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) { if (key == MyGUI::KeyCode::ArrowUp) 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)); else return; onHourSliderChangedPosition(mHourSlider, mHourSlider->getScrollPosition()); } void WaitDialog::onWaitingProgressChanged(int cur, int total) { mProgressBar.setProgress(cur, total); MWBase::Environment::get().getMechanicsManager()->rest(1, mSleeping); MWBase::Environment::get().getWorld()->advanceTime(1); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); if (player.getClass().getCreatureStats(player).isDead()) stopWaiting(); } void WaitDialog::onWaitingInterrupted() { MWBase::Environment::get().getWindowManager()->messageBox("#{sSleepInterrupt}"); MWBase::Environment::get().getWorld()->spawnRandomCreature(mInterruptCreatureList); stopWaiting(); } void WaitDialog::onWaitingFinished() { stopWaiting(); MWWorld::Ptr player = MWMechanics::getPlayer(); const MWMechanics::NpcStats& pcstats = player.getClass().getNpcStats(player); // trigger levelup if possible 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); } } 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()); 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}")); mSleeping = canRest; Gui::Box* box = dynamic_cast(mMainWidget); if (box == nullptr) throw std::runtime_error("main widget must be a box"); box->notifyChildrenSizeChanged(); center(); } void WaitDialog::onFrame(float dt) { mTimeAdvancer.onFrame(dt); if (mFadeTimeRemaining <= 0) return; mFadeTimeRemaining -= dt; if (mFadeTimeRemaining <= 0) { mProgressBar.setVisible(true); mTimeAdvancer.run(mHours, mInterruptAt); } } void WaitDialog::stopWaiting() { MWBase::Environment::get().getWindowManager()->fadeScreenIn(0.2f); mProgressBar.setVisible(false); MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Rest); mTimeAdvancer.stop(); } void WaitDialog::wakeUp() { mSleeping = false; if (mInterruptAt != -1) onWaitingInterrupted(); else stopWaiting(); } } openmw-openmw-0.49.0/apps/openmw/mwgui/waitdialog.hpp000066400000000000000000000042531503074453300226610ustar00rootroot00000000000000#ifndef MWGUI_WAIT_DIALOG_H #define MWGUI_WAIT_DIALOG_H #include "timeadvancer.hpp" #include "windowbase.hpp" #include namespace MWGui { class WaitDialogProgressBar : public WindowBase { public: WaitDialogProgressBar(); void onOpen() override; void setProgress(int cur, int total); protected: MyGUI::ProgressBar* mProgressBar; MyGUI::TextBox* mProgressText; }; class WaitDialog : public WindowBase { public: WaitDialog(); void setPtr(const MWWorld::Ptr& ptr) override; void onOpen() override; bool exit() override; void clear() override; void onFrame(float dt) override; bool getSleeping() { return mTimeAdvancer.isRunning() && mSleeping; } void wakeUp(); void autosave(); WindowBase* getProgressBar() { return &mProgressBar; } std::string_view getWindowIdForLua() const override { return "WaitDialog"; } protected: MyGUI::TextBox* mDateTimeText; MyGUI::TextBox* mRestText; MyGUI::TextBox* mHourText; MyGUI::Button* mUntilHealedButton; MyGUI::Button* mWaitButton; MyGUI::Button* mCancelButton; MyGUI::ScrollBar* mHourSlider; TimeAdvancer mTimeAdvancer; bool mSleeping; int mHours; int mManualHours; // stores the hours to rest selected via slider float mFadeTimeRemaining; int mInterruptAt; ESM::RefId mInterruptCreatureList; WaitDialogProgressBar mProgressBar; void onUntilHealedButtonClicked(MyGUI::Widget* sender); void onWaitButtonClicked(MyGUI::Widget* sender); void onCancelButtonClicked(MyGUI::Widget* sender); void onHourSliderChangedPosition(MyGUI::ScrollBar* sender, size_t position); void onKeyButtonPressed(MyGUI::Widget* sender, MyGUI::KeyCode key, MyGUI::Char character); void onWaitingProgressChanged(int cur, int total); void onWaitingInterrupted(); void onWaitingFinished(); void setCanRest(bool canRest); void startWaiting(int hoursToWait); void stopWaiting(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/widgets.cpp000066400000000000000000000426101503074453300221750ustar00rootroot00000000000000#include "widgets.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwworld/esmstore.hpp" namespace MWGui::Widgets { /* MWSkill */ MWSkill::MWSkill() : mSkillNameWidget(nullptr) , mSkillValueWidget(nullptr) { } void MWSkill::setSkillId(ESM::RefId skill) { mSkillId = skill; updateWidgets(); } void MWSkill::setSkillValue(const SkillValue& value) { mValue = value; updateWidgets(); } void MWSkill::updateWidgets() { if (mSkillNameWidget) { const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().search(mSkillId); if (skill == nullptr) mSkillNameWidget->setCaption({}); else mSkillNameWidget->setCaption(skill->mName); } 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); } MWSkill::~MWSkill() {} void MWSkill::initialiseOverride() { 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); } } /* MWAttribute */ MWAttribute::MWAttribute() : mAttributeNameWidget(nullptr) , mAttributeValueWidget(nullptr) { } void MWAttribute::setAttributeId(ESM::RefId attributeId) { mId = attributeId; updateWidgets(); } void MWAttribute::setAttributeValue(const AttributeValue& value) { mValue = value; updateWidgets(); } void MWAttribute::onClicked(MyGUI::Widget* _sender) { eventClicked(this); } void MWAttribute::updateWidgets() { if (mAttributeNameWidget) { const ESM::Attribute* attribute = MWBase::Environment::get().getESMStore()->get().search(mId); if (!attribute) { mAttributeNameWidget->setCaption({}); } else { mAttributeNameWidget->setCaption(MyGUI::UString(attribute->mName)); } } 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); } } /* MWSpell */ MWSpell::MWSpell() : mSpellNameWidget(nullptr) { } void MWSpell::setSpellId(const ESM::RefId& spellId) { mId = spellId; updateWidgets(); } 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::updateWidgets() { if (mSpellNameWidget && MWBase::Environment::get().getWindowManager()) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const ESM::Spell* spell = store.get().search(mId); if (spell) mSpellNameWidget->setCaption(spell->mName); else mSpellNameWidget->setCaption({}); } } void MWSpell::initialiseOverride() { Base::initialiseOverride(); assignWidget(mSpellNameWidget, "StatName"); } MWSpell::~MWSpell() {} /* MWEffectList */ MWEffectList::MWEffectList() : mEffectList(0) { } 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) { 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(); } // ... then adjust the size for all widgets for (MyGUI::Widget* effectWidget : effects) { 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); } } // inform the parent about width coord.width = maxwidth; } void MWEffectList::updateWidgets() {} void MWEffectList::initialiseOverride() { Base::initialiseOverride(); } MWEffectList::~MWEffectList() {} 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::MWSpellEffect() : mImageWidget(nullptr) , mTextWidget(nullptr) , mRequestedWidth(0) { } void MWSpellEffect::setSpellEffect(const SpellEffectParams& params) { mEffectParams = params; updateWidgets(); } void MWSpellEffect::updateWidgets() { 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; } const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); 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); auto windowManager = MWBase::Environment::get().getWindowManager(); 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 spellLine = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill); if ((mEffectParams.mMagnMin || mEffectParams.mMagnMax) && !mEffectParams.mNoMagnitude) { ESM::MagicEffect::MagnitudeDisplayType displayType = magicEffect->getMagnitudeDisplayType(); if (displayType == ESM::MagicEffect::MDT_TimesInt) { 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; spellLine += formatter.str(); } else if (displayType != ESM::MagicEffect::MDT_None) { 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 += ' '; spellLine += ft; } else if (displayType == ESM::MagicEffect::MDT_Level) { spellLine += ' '; if (mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) spellLine += lvl; else spellLine += lvls; } else // ESM::MagicEffect::MDT_Points { spellLine += ' '; if (mEffectParams.mMagnMin == mEffectParams.mMagnMax && std::abs(mEffectParams.mMagnMin) == 1) spellLine += pt; else spellLine += pts; } } } // 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); 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}"; } // 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", {}); } } 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(); 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)); } 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)); } MWDynamicStat::~MWDynamicStat() {} void MWDynamicStat::initialiseOverride() { Base::initialiseOverride(); assignWidget(mTextWidget, "Text"); assignWidget(mBarWidget, "Bar"); assignWidget(mBarTextWidget, "BarText"); } } openmw-openmw-0.49.0/apps/openmw/mwgui/widgets.hpp000066400000000000000000000220431503074453300222000ustar00rootroot00000000000000#ifndef MWGUI_WIDGETS_H #define MWGUI_WIDGETS_H #include "../mwmechanics/stat.hpp" #include #include #include #include #include #include #include namespace MyGUI { class ImageBox; class ControllerItem; } namespace MWBase { class WindowManager; } /* This file contains various custom widgets used in OpenMW. */ namespace MWGui { namespace Widgets { class MWEffectList; struct SpellEffectParams { SpellEffectParams() : mNoTarget(false) , mIsConstant(false) , mNoMagnitude(false) , mKnown(true) , mEffectID(-1) , mMagnMin(-1) , mMagnMax(-1) , mRange(-1) , mDuration(-1) , mArea(0) { } bool mNoTarget; // potion effects for example have no target (target is always the player) bool mIsConstant; // constant effect means that duration will not be displayed bool mNoMagnitude; // effect magnitude will not be displayed (e.g ingredients) bool mKnown; // is this effect known to the player? (If not, will display as a question mark instead) // value of -1 here means the effect is unknown to the player short mEffectID; ESM::RefId mSkill, mAttribute; // value of -1 here means the value is unavailable int mMagnMin, mMagnMax, mRange, mDuration; // value of 0 -> no area effect int mArea; bool operator==(const SpellEffectParams& other) const { 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 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); } }; typedef std::vector SpellEffectList; class MWSkill final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWSkill) public: MWSkill(); typedef MWMechanics::Stat SkillValue; void setSkillId(ESM::RefId skillId); void setSkillValue(const SkillValue& value); ESM::RefId getSkillId() const { return mSkillId; } const SkillValue& getSkillValue() const { return mValue; } // Events typedef MyGUI::delegates::MultiDelegate EventHandle_SkillVoid; /** Event : Skill clicked.\n signature : void method(MWSkill* _sender)\n */ EventHandle_SkillVoid eventClicked; protected: virtual ~MWSkill(); void initialiseOverride() override; void onClicked(MyGUI::Widget* _sender); private: void updateWidgets(); ESM::RefId mSkillId; SkillValue mValue; MyGUI::TextBox* mSkillNameWidget; MyGUI::TextBox* mSkillValueWidget; }; typedef MWSkill* MWSkillPtr; class MWAttribute final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWAttribute) public: MWAttribute(); typedef MWMechanics::AttributeValue AttributeValue; void setAttributeId(ESM::RefId attributeId); void setAttributeValue(const AttributeValue& value); ESM::RefId getAttributeId() const { return mId; } const AttributeValue& getAttributeValue() const { return mValue; } // Events typedef MyGUI::delegates::MultiDelegate EventHandle_AttributeVoid; /** Event : Attribute clicked.\n signature : void method(MWAttribute* _sender)\n */ EventHandle_AttributeVoid eventClicked; protected: ~MWAttribute() override = default; void initialiseOverride() override; void onClicked(MyGUI::Widget* _sender); private: void updateWidgets(); ESM::RefId mId; AttributeValue mValue; MyGUI::TextBox* mAttributeNameWidget; MyGUI::TextBox* mAttributeValueWidget; }; typedef MWAttribute* MWAttributePtr; /** * @todo remove this class and use MWEffectList instead */ class MWSpellEffect; class MWSpell final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWSpell) public: MWSpell(); 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 various flags, see MWEffectList::EffectFlags */ void createEffectWidgets( std::vector& effects, MyGUI::Widget* creator, MyGUI::IntCoord& coord, int flags); const ESM::RefId& getSpellId() const { return mId; } protected: virtual ~MWSpell(); void initialiseOverride() override; private: void updateWidgets(); ESM::RefId mId; MyGUI::TextBox* mSpellNameWidget; }; typedef MWSpell* MWSpellPtr; class MWEffectList final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWEffectList) public: MWEffectList(); enum EffectFlags { EF_NoTarget = 0x01, // potions have no target (target is always the player) EF_Constant = 0x02, // constant effect means that duration will not be displayed EF_NoMagnitude = 0x04 // ingredients have no magnitude }; void setEffectList(const SpellEffectList& list); static SpellEffectList effectListFromESM(const ESM::EffectList* effects); /** * @param vector to store the created effect widgets * @param parent widget * @param coordinates to use, will be expanded if more space is needed * @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); protected: virtual ~MWEffectList(); void initialiseOverride() override; private: void updateWidgets(); SpellEffectList mEffectList; }; typedef MWEffectList* MWEffectListPtr; class MWSpellEffect final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWSpellEffect) public: MWSpellEffect(); typedef ESM::ENAMstruct SpellEffectValue; void setSpellEffect(const SpellEffectParams& params); int getRequestedWidth() const { return mRequestedWidth; } protected: virtual ~MWSpellEffect(); void initialiseOverride() override; private: static constexpr int sIconOffset = 24; void updateWidgets(); SpellEffectParams mEffectParams; MyGUI::ImageBox* mImageWidget; MyGUI::TextBox* mTextWidget; int mRequestedWidth; }; typedef MWSpellEffect* MWSpellEffectPtr; class MWDynamicStat final : public MyGUI::Widget { MYGUI_RTTI_DERIVED(MWDynamicStat) public: MWDynamicStat(); void setValue(int value, int max); void setTitle(std::string_view text); int getValue() const { return mValue; } int getMax() const { return mMax; } protected: virtual ~MWDynamicStat(); void initialiseOverride() override; private: int mValue, mMax; MyGUI::TextBox* mTextWidget; MyGUI::ProgressBar* mBarWidget; MyGUI::TextBox* mBarTextWidget; }; typedef MWDynamicStat* MWDynamicStatPtr; } } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/windowbase.cpp000066400000000000000000000125611503074453300226730ustar00rootroot00000000000000#include "windowbase.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include #include "draganddrop.hpp" #include "exposedwindow.hpp" using namespace MWGui; WindowBase::WindowBase(std::string_view parLayout) : Layout(parLayout) { mMainWidget->setVisible(false); Window* window = mMainWidget->castType(false); if (!window) return; MyGUI::Button* button = nullptr; MyGUI::VectorWidgetPtr widgets = window->getSkinWidgetsByName("Action"); for (MyGUI::Widget* widget : widgets) { if (widget->isUserString("SupportDoubleClick")) button = widget->castType(); } if (button) button->eventMouseButtonDoubleClick += MyGUI::newDelegate(this, &WindowBase::onDoubleClick); } void WindowBase::onTitleDoubleClicked() { if (MyGUI::InputManager::getInstance().isShiftPressed()) MWBase::Environment::get().getWindowManager()->toggleMaximized(this); } void WindowBase::onDoubleClick(MyGUI::Widget* _sender) { onTitleDoubleClicked(); } void WindowBase::setVisible(bool visible) { visible = visible && !mDisabledByLua; bool wasVisible = mMainWidget->getVisible(); mMainWidget->setVisible(visible); if (visible) onOpen(); else if (wasVisible) onClose(); } bool WindowBase::isVisible() const { return mMainWidget->getVisible(); } void WindowBase::center() { // Centre dialog MyGUI::IntSize layerSize = MyGUI::RenderManager::getInstance().getViewSize(); if (mMainWidget->getLayer()) layerSize = mMainWidget->getLayer()->getSize(); MyGUI::IntCoord coord = mMainWidget->getCoord(); 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) { } void WindowModal::onOpen() { 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().setKeyFocusWidget(focus); } void WindowModal::onClose() { MWBase::Environment::get().getWindowManager()->removeCurrentModal(this); MyGUI::InputManager::getInstance().removeWidgetModal(mMainWidget); } NoDrop::NoDrop(DragAndDrop* drag, MyGUI::Widget* widget) : mWidget(widget) , mDrag(drag) , mTransparent(false) { } void NoDrop::onFrame(float dt) { if (!mWidget) return; MyGUI::IntPoint mousePos = MyGUI::InputManager::getInstance().getMousePosition(); if (mDrag->mIsOnDragAndDrop) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getMouseFocusWidget(); while (focus && focus != mWidget) focus = focus->getParent(); if (focus == mWidget) mTransparent = true; } if (!mWidget->getAbsoluteCoord().inside(mousePos)) mTransparent = false; if (mTransparent) { mWidget->setNeedMouseFocus(false); // Allow click-through setAlpha(std::max(0.13f, mWidget->getAlpha() - dt * 5)); } else { mWidget->setNeedMouseFocus(true); setAlpha(std::min(1.0f, mWidget->getAlpha() + dt * 5)); } } void NoDrop::setAlpha(float alpha) { if (mWidget) mWidget->setAlpha(alpha); } BookWindowBase::BookWindowBase(std::string_view parLayout) : WindowBase(parLayout) { } float BookWindowBase::adjustButton(std::string_view name) { Gui::ImageButton* button; WindowBase::getWidget(button, name); MyGUI::IntSize requested = button->getRequestedSize(); float scale = float(requested.height) / button->getSize().height; MyGUI::IntSize newSize = requested; newSize.width /= scale; newSize.height /= scale; button->setSize(newSize); if (button->getAlign().isRight()) { MyGUI::IntSize diff = (button->getSize() - requested); diff.width /= scale; diff.height /= scale; button->setPosition(button->getPosition() + MyGUI::IntPoint(diff.width, 0)); } return scale; } openmw-openmw-0.49.0/apps/openmw/mwgui/windowbase.hpp000066400000000000000000000054121503074453300226750ustar00rootroot00000000000000#ifndef MWGUI_WINDOW_BASE_H #define MWGUI_WINDOW_BASE_H #include "layout.hpp" namespace MWWorld { class Ptr; } namespace MWGui { class DragAndDrop; class WindowBase : public Layout { public: WindowBase(std::string_view parLayout); virtual MyGUI::Widget* getDefaultKeyFocus() { return nullptr; } // Events typedef MyGUI::delegates::MultiDelegate EventHandle_WindowBase; /// Open this object in the GUI, for windows that support it virtual void setPtr(const MWWorld::Ptr& ptr) {} /// Called every frame if the window is in an active GUI mode virtual void onFrame(float duration) {} /// Notify that window has been made visible virtual void onOpen() {} /// Notify that window has been hidden virtual void onClose() {} /// Gracefully exits the window 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() const; void center(); /// Clear any state specific to the running game virtual void clear() {} /// 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; }; /* * "Modal" windows cause the rest of the interface to be inaccessible while they are visible */ class WindowModal : public WindowBase { public: WindowModal(const std::string& parLayout); void onOpen() override; void onClose() override; bool exit() override { return true; } }; /// A window that cannot be the target of a drag&drop action. /// When hovered with a drag item, the window will become transparent and allow click-through. class NoDrop { public: NoDrop(DragAndDrop* drag, MyGUI::Widget* widget); void onFrame(float dt); virtual void setAlpha(float alpha); virtual ~NoDrop() = default; private: MyGUI::Widget* mWidget; DragAndDrop* mDrag; bool mTransparent; }; class BookWindowBase : public WindowBase { public: BookWindowBase(std::string_view parLayout); protected: float adjustButton(std::string_view name); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwgui/windowmanagerimp.cpp000066400000000000000000002444511503074453300241060ustar00rootroot00000000000000#include "windowmanagerimp.hpp" #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 "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/world.hpp" #include "../mwrender/vismask.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/globals.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwrender/postprocessor.hpp" #include "alchemywindow.hpp" #include "backgroundimage.hpp" #include "bookpage.hpp" #include "bookwindow.hpp" #include "companionwindow.hpp" #include "confirmationdialog.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 "exposedwindow.hpp" #include "hud.hpp" #include "inventorywindow.hpp" #include "itemchargeview.hpp" #include "itemview.hpp" #include "itemwidget.hpp" #include "jailscreen.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 { 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, bool exportFonts, 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 = std::make_unique(encoding, resourceSystem->getVFS(), mScalingFactor, exportFonts); // Register own widgets with MyGUI MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Widget"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); MyGUI::FactoryManager::getInstance().registerFactory("Layer"); 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::ResourceManager::getInstance().load("core.xml"); const bool keyboardNav = Settings::gui().mKeyboardNavigation; mKeyboardNavigation = std::make_unique(); mKeyboardNavigation->setEnabled(keyboardNav); Gui::ImageButton::setDefaultNeedKeyFocus(keyboardNav); auto loadingScreen = std::make_unique(mResourceSystem, mViewer); mLoadingScreen = loadingScreen.get(); mWindows.push_back(std::move(loadingScreen)); // set up the hardware cursor manager mCursorManager = std::make_unique(); MyGUI::PointerManager::getInstance().eventChangeMousePointer += MyGUI::newDelegate(this, &WindowManager::onCursorChange); MyGUI::InputManager::getInstance().eventChangeKeyFocus += MyGUI::newDelegate(this, &WindowManager::onKeyFocusChanged); // Create all cursors in advance createCursors(); onCursorChange(MyGUI::PointerManager::getInstance().getDefaultPointer()); mCursorManager->setEnabled(true); // hide mygui's pointer MyGUI::PointerManager::getInstance().setVisible(false); 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->setNeedMouseFocus(true); mVideoWidget->setNeedKeyFocus(true); mVideoWidget->setVFS(resourceSystem->getVFS()); // Removes default MyGUI system clipboard implementation, which supports windows only 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); mVideoWrapper = std::make_unique(window, viewer); mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); if (useShaders) mGuiPlatform->getRenderManagerPtr()->enableShaders(mResourceSystem->getSceneManager()->getShaderManager()); mStatsWatcher = std::make_unique(); } void WindowManager::initUI() { // Get size info from the Gui object int w = MyGUI::RenderManager::getInstance().getViewSize().width; int h = MyGUI::RenderManager::getInstance().getViewSize().height; mTextColours.loadColours(); mDragAndDrop = std::make_unique(); auto recharge = std::make_unique(); mGuiModeStates[GM_Recharge] = GuiModeState(recharge.get()); mWindows.push_back(std::move(recharge)); auto menu = std::make_unique(w, h, mResourceSystem->getVFS(), mVersionDescription); mGuiModeStates[GM_MainMenu] = GuiModeState(menu.get()); mWindows.push_back(std::move(menu)); 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, 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); 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 }); auto hud = std::make_unique(mCustomMarkers, mDragAndDrop.get(), mLocalMapRender.get()); mHud = hud.get(); mWindows.push_back(std::move(hud)); mToolTips = std::make_unique(); auto scrollWindow = std::make_unique(); mScrollWindow = scrollWindow.get(); mWindows.push_back(std::move(scrollWindow)); mGuiModeStates[GM_Scroll] = GuiModeState(mScrollWindow); auto bookWindow = std::make_unique(); mBookWindow = bookWindow.get(); mWindows.push_back(std::move(bookWindow)); mGuiModeStates[GM_Book] = GuiModeState(mBookWindow); auto countDialog = std::make_unique(); mCountDialog = countDialog.get(); mWindows.push_back(std::move(countDialog)); auto settingsWindow = std::make_unique(); mSettingsWindow = settingsWindow.get(); mWindows.push_back(std::move(settingsWindow)); trackWindow(mSettingsWindow, makeSettingsWindowSettingValues()); auto confirmationDialog = std::make_unique(); mConfirmationDialog = confirmationDialog.get(); mWindows.push_back(std::move(confirmationDialog)); auto alchemyWindow = std::make_unique(); trackWindow(alchemyWindow.get(), makeAlchemyWindowSettingValues()); mGuiModeStates[GM_Alchemy] = GuiModeState(alchemyWindow.get()); mWindows.push_back(std::move(alchemyWindow)); auto quickKeysMenu = std::make_unique(); mQuickKeysMenu = quickKeysMenu.get(); mWindows.push_back(std::move(quickKeysMenu)); mGuiModeStates[GM_QuickKeysMenu] = GuiModeState(mQuickKeysMenu); auto levelupDialog = std::make_unique(); mGuiModeStates[GM_Levelup] = GuiModeState(levelupDialog.get()); mWindows.push_back(std::move(levelupDialog)); auto waitDialog = std::make_unique(); mWaitDialog = waitDialog.get(); mWindows.push_back(std::move(waitDialog)); mGuiModeStates[GM_Rest] = GuiModeState({ mWaitDialog->getProgressBar(), mWaitDialog }); auto spellCreationDialog = std::make_unique(); mGuiModeStates[GM_SpellCreation] = GuiModeState(spellCreationDialog.get()); mWindows.push_back(std::move(spellCreationDialog)); auto enchantingDialog = std::make_unique(); mGuiModeStates[GM_Enchanting] = GuiModeState(enchantingDialog.get()); mWindows.push_back(std::move(enchantingDialog)); auto trainingWindow = std::make_unique(); mGuiModeStates[GM_Training] = GuiModeState({ trainingWindow->getProgressBar(), trainingWindow.get() }); mWindows.push_back(std::move(trainingWindow)); auto merchantRepair = std::make_unique(); mGuiModeStates[GM_MerchantRepair] = GuiModeState(merchantRepair.get()); mWindows.push_back(std::move(merchantRepair)); auto repair = std::make_unique(); mGuiModeStates[GM_Repair] = GuiModeState(repair.get()); mWindows.push_back(std::move(repair)); mSoulgemDialog = std::make_unique(mMessageBoxManager.get()); 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)); 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)) { auto werewolfFader = std::make_unique(werewolfFaderTex); mWerewolfFader = werewolfFader.get(); mWindows.push_back(std::move(werewolfFader)); } 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)) { hitFaderTexture = "textures\\player_hit_01.dds"; hitFaderCoord = MyGUI::FloatCoord(0.2, 0.25, 0.6, 0.5); } 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)); auto debugWindow = std::make_unique(); mDebugWindow = debugWindow.get(); mWindows.push_back(std::move(debugWindow)); trackWindow(mDebugWindow, makeDebugWindowSettingValues()); 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"); mHud->setVisible(true); mCharGen = std::make_unique(mViewer->getSceneData()->asGroup(), mResourceSystem); updatePinnedWindows(); // Set up visibility updateVisible(); mStatsWatcher->addListener(mHud); mStatsWatcher->addListener(mStatsWindow); mStatsWatcher->addListener(mCharGen.get()); for (auto& window : mWindows) { std::string_view id = window->getWindowIdForLua(); if (!id.empty()) mLuaIdToWindow.emplace(id, window.get()); } } void WindowManager::setNewGame(bool newgame) { if (newgame) { disallowAll(); mStatsWatcher->removeListener(mCharGen.get()); mCharGen = std::make_unique(mViewer->getSceneData()->asGroup(), mResourceSystem); mStatsWatcher->addListener(mCharGen.get()); } else allow(GW_ALL); mStatsWatcher->forceUpdate(); } WindowManager::~WindowManager() { try { LuaUi::clearGameInterface(); LuaUi::clearMenuInterface(); mStatsWatcher.reset(); MyGUI::LanguageManager::getInstance().eventRequestTag.clear(); MyGUI::PointerManager::getInstance().eventChangeMousePointer.clear(); MyGUI::InputManager::getInstance().eventChangeKeyFocus.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardChanged.clear(); MyGUI::ClipboardManager::getInstance().eventClipboardRequested.clear(); mWindows.clear(); mMessageBoxManager.reset(); mToolTips.reset(); mCharGen.reset(); mKeyboardNavigation.reset(); cleanupGarbage(); mFontLoader.reset(); mGui->shutdown(); mGuiPlatform->shutdown(); } catch (const MyGUI::Exception& e) { Log(Debug::Error) << "Error in the destructor: " << e.what(); } } void WindowManager::setStore(const MWWorld::ESMStore& store) { mStore = &store; } void WindowManager::cleanupGarbage() { // Delete any dialogs which are no longer in use mGarbageDialogs.clear(); } void WindowManager::enableScene(bool enable) { unsigned int disablemask = MWRender::Mask_GUI | MWRender::Mask_PreCompile; if (!enable && getCullMask() != disablemask) { mOldUpdateMask = mViewer->getUpdateVisitor()->getTraversalMask(); mOldCullMask = getCullMask(); mViewer->getUpdateVisitor()->setTraversalMask(disablemask); setCullMask(disablemask); } else if (enable && getCullMask() == disablemask) { mViewer->getUpdateVisitor()->setTraversalMask(mOldUpdateMask); setCullMask(mOldCullMask); } } void WindowManager::updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) { mConsole->updateSelectedObjectPtr(currentPtr, newPtr); } void WindowManager::updateVisible() { bool loading = (getMode() == GM_Loading || getMode() == GM_LoadingWallpaper); bool mainmenucover = containsMode(GM_MainMenu) && MWBase::Environment::get().getStateManager()->getState() == MWBase::StateManager::State_NoGame; enableScene(!loading && !mainmenucover); if (!mMap) return; // UI not created yet mHud->setVisible(mHudEnabled && !loading); mToolTips->setVisible(mHudEnabled && !loading); bool gameMode = !isGuiMode(); MWBase::Environment::get().getInputManager()->changeInputMode(!gameMode); mInputBlocker->setVisible(gameMode); if (loading) setCursorVisible(mMessageBoxManager && mMessageBoxManager->isInteractiveMessageBox()); else setCursorVisible(!gameMode); if (gameMode) 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))); setSpellVisibility((mAllowed & GW_Magic) && (!mSpellWindow->pinned() || (mForceHidden & GW_Magic))); setHMSVisibility((mAllowed & GW_Stats) && (!mStatsWindow->pinned() || (mForceHidden & GW_Stats))); mInventoryWindow->setGuiMode(getMode()); // If in game mode (or interactive messagebox), show the pinned windows 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)); return; } else if (getMode() != GM_Inventory) { mMap->setVisible(false); mStatsWindow->setVisible(false); mSpellWindow->setVisible(false); mHud->setDrowningBarVisible(false); mInventoryWindow->setVisible( getMode() == GM_Container || getMode() == GM_Barter || getMode() == GM_Companion); } GuiMode mode = mGuiModes.back(); mInventoryWindow->setTrading(mode == GM_Barter); if (getMode() == GM_Inventory) { // For the inventory mode, compute the effective set of windows to show. // This is controlled both by what windows the // user has opened/closed (the 'shown' variable) and by what // windows we are allowed to show (the 'allowed' var.) int eff = mShown & mAllowed & ~mForceHidden; mMap->setVisible(eff & GW_Map); mInventoryWindow->setVisible(eff & GW_Inventory); mSpellWindow->setVisible(eff & GW_Magic); mStatsWindow->setVisible(eff & GW_Stats); } 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; } } void WindowManager::setDrowningTimeLeft(float time, float maxTime) { mHud->setDrowningTimeLeft(time, maxTime); } void WindowManager::removeDialog(std::unique_ptr&& dialog) { if (!dialog) return; dialog->setVisible(false); mGarbageDialogs.push_back(std::move(dialog)); } void WindowManager::exitCurrentGuiMode() { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->finish(); return; } if (mGuiModes.empty()) return; GuiModeState& state = mGuiModeStates[mGuiModes.back()]; 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); return; } } popGuiMode(); } void WindowManager::interactiveMessageBox( std::string_view message, const std::vector& buttons, bool block, int defaultFocus) { mMessageBoxManager->createInteractiveMessageBox(message, buttons, block, defaultFocus); updateVisible(); if (block) { Misc::FrameRateLimiter frameRateLimiter = Misc::makeFrameRateLimiter(MWBase::Environment::get().getFrameRateLimit()); while (mMessageBoxManager->readPressedButton(false) == -1 && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()) .count(); mKeyboardNavigation->onFrame(); mMessageBoxManager->onFrame(dt); MWBase::Environment::get().getInputManager()->update(dt, true, false); if (!mWindowVisible) std::this_thread::sleep_for(std::chrono::milliseconds(5)); else { mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); frameRateLimiter.limit(); } mMessageBoxManager->resetInteractiveMessageBox(); } } void WindowManager::messageBox(std::string_view message, enum MWGui::ShowInDialogueMode showInDialogueMode) { 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::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); } void WindowManager::removeStaticMessageBox() { mMessageBoxManager->removeStaticMessageBox(); } int WindowManager::readPressedButton() { return mMessageBoxManager->readPressedButton(); } std::string_view WindowManager::getGameSettingString(std::string_view id, std::string_view default_) { const ESM::GameSetting* setting = mStore->get().search(id); if (setting && setting->mValue.getType() == ESM::VT_String) return setting->mValue.getString(); return default_; } void WindowManager::updateMap() { if (!mLocalMapRender) return; 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::Vec3f playerdirection; int x, y; float u, v; mLocalMapRender->updatePlayer(playerPosition, playerOrientation, u, v, x, y, playerdirection); if (!player.getCell()->isExterior()) { setActiveMap(*player.getCell()->getCell()); } // else: need to know the current grid center, call setActiveMap from changeCell mMap->setPlayerDir(playerdirection.x(), playerdirection.y()); mMap->setPlayerPos(x, y, u, v); mHud->setPlayerDir(playerdirection.x(), playerdirection.y()); mHud->setPlayerPos(x, y, u, v); } void WindowManager::update(float frameDuration) { handleScheduledMessageBoxes(); bool gameRunning = MWBase::Environment::get().getStateManager()->getState() != MWBase::StateManager::State_NoGame; if (gameRunning) updateMap(); if (!mGuiModes.empty()) { GuiModeState& state = mGuiModeStates[mGuiModes.back()]; for (WindowBase* window : state.mWindows) window->onFrame(frameDuration); } else { // update pinned windows if visible for (WindowBase* window : mGuiModeStates[GM_Inventory].mWindows) if (window->isVisible()) window->onFrame(frameDuration); } // 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()) { std::vector::iterator found = std::find( mCurrentModals.begin(), mCurrentModals.end(), mMessageBoxManager->getInteractiveMessageBox()); if (found != mCurrentModals.end()) { WindowModal* msgbox = *found; std::swap(*found, mCurrentModals.back()); MyGUI::InputManager::getInstance().addWidgetModal(msgbox->mMainWidget); mKeyboardNavigation->setModalWindow(msgbox->mMainWidget); mKeyboardNavigation->setDefaultFocus(msgbox->mMainWidget, msgbox->getDefaultKeyFocus()); } } if (!mCurrentModals.empty()) mCurrentModals.back()->onFrame(frameDuration); mKeyboardNavigation->onFrame(); if (mMessageBoxManager) mMessageBoxManager->onFrame(frameDuration); mToolTips->onFrame(frameDuration); 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(); const MWWorld::Class& playerCls = player.getClass(); int currentBounty = playerCls.getNpcStats(player).getBounty(); if (currentBounty != mPlayerBounty) { if (mPlayerBounty >= 0 && currentBounty > mPlayerBounty) messageBox("#{sCrimeMessage}"); 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); mPostProcessorHud->onFrame(frameDuration); if (mCharGen) mCharGen->onFrame(frameDuration); updateActivatedQuickKey(); mStatsWatcher->update(); cleanupGarbage(); } void WindowManager::changeCell(const MWWorld::CellStore* cell) { mMap->requestMapRender(cell); std::string name{ MWBase::Environment::get().getWorld()->getCellName(cell) }; mMap->setCellName(name); mHud->setCellName(name); auto cellCommon = cell->getCell(); if (cellCommon->isExterior()) { if (!cellCommon->getNameId().empty()) mMap->addVisitedLocation(name, cellCommon->getGridX(), cellCommon->getGridY()); mMap->cellExplored(cellCommon->getGridX(), cellCommon->getGridY()); } else { 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(*cellCommon); } void WindowManager::setActiveMap(const MWWorld::Cell& cell) { mMap->setActiveCell(cell); mHud->setActiveCell(cell); } void WindowManager::setDrowningBarVisibility(bool visible) { mHud->setDrowningBarVisible(visible); } void WindowManager::setHMSVisibility(bool visible) { mHud->setHmsVisible(visible); } void WindowManager::setMinimapVisibility(bool visible) { mHud->setMinimapVisible(visible); } bool WindowManager::toggleFogOfWar() { mMap->toggleFogOfWar(); return mHud->toggleFogOfWar(); } void WindowManager::setFocusObject(const MWWorld::Ptr& focus) { mToolTips->setFocusObject(focus); const int showOwned = Settings::game().mShowOwned; if (mHud && (showOwned == 2 || showOwned == 3)) { bool owned = mToolTips->checkOwned(); mHud->setCrosshairOwned(owned); } } void WindowManager::setFocusObjectScreenCoords(float x, float y) { mToolTips->setFocusObjectScreenCoords(x, y); } bool WindowManager::toggleFullHelp() { return mToolTips->toggleFullHelp(); } bool WindowManager::getFullHelp() const { return mToolTips->getFullHelp(); } void WindowManager::setWeaponVisibility(bool visible) { mHud->setWeapVisible(visible); } void WindowManager::setSpellVisibility(bool visible) { mHud->setSpellVisible(visible); mHud->setEffectVisible(visible); } void WindowManager::setSneakVisibility(bool visible) { mHud->setSneakVisible(visible); } void WindowManager::setDragDrop(bool dragDrop) { mToolTips->setEnabled(!dragDrop); MWBase::Environment::get().getInputManager()->setDragDrop(dragDrop); } void WindowManager::setCursorVisible(bool visible) { mCursorVisible = visible; } void WindowManager::setCursorActive(bool active) { mCursorActive = active; } void WindowManager::onRetrieveTag(const MyGUI::UString& _tag, MyGUI::UString& _result) { std::string_view tag = _tag; std::string_view MyGuiPrefix = "setting="; std::string_view tokenToFind = "sCell="; if (tag.starts_with(MyGuiPrefix)) { tag = tag.substr(MyGuiPrefix.length()); size_t comma_pos = tag.find(','); 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.starts_with(tokenToFind)) { 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)) { return; } 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 << "'"; _result.assign(tag.data(), tag.size()); return; } const ESM::GameSetting* setting = mStore->get().search(tag); if (setting && setting->mValue.getType() == ESM::VT_String) _result = setting->mValue.getString(); else _result.assign(tag.data(), tag.size()); } } void WindowManager::processChangedSettings(const Settings::CategorySettingVector& changed) { bool changeRes = false; for (const auto& setting : changed) { 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 mode") mVideoWrapper->setSyncToVBlank(Settings::video().mVsyncMode); else if (setting.first == "Video" && (setting.second == "gamma" || setting.second == "contrast")) mVideoWrapper->setGammaContrast(Settings::video().mGamma, Settings::video().mContrast); } if (changeRes) { mVideoWrapper->setVideoMode(Settings::video().mResolutionX, Settings::video().mResolutionY, Settings::video().mWindowMode, Settings::video().mWindowBorder); } } void WindowManager::windowResized(int x, int y) { 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); // scaled size const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); x = viewSize.width; y = viewSize.height; sizeVideo(x, y); if (!mHud) return; // UI not initialized yet for (const auto& [window, settings] : mTrackedWindows) { 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))); WindowBase::clampWindowCoordinates(window); } for (const auto& window : mWindows) window->onResChange(x, y); // TODO: check if any windows are now off-screen and move them back if so } bool WindowManager::isWindowVisible() const { return mWindowVisible; } void WindowManager::windowVisibilityChange(bool visible) { mWindowVisible = visible; } void WindowManager::windowClosed() { MWBase::Environment::get().getStateManager()->requestQuit(); } void WindowManager::onCursorChange(std::string_view name) { mCursorManager->cursorChanged(name); } void WindowManager::pushGuiMode(GuiMode mode) { pushGuiMode(mode, MWWorld::Ptr()); } void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg) { pushGuiMode(mode, arg, false); } void WindowManager::forceLootMode(const MWWorld::Ptr& ptr) { pushGuiMode(MWGui::GM_Container, ptr, true); } void WindowManager::pushGuiMode(GuiMode mode, const MWWorld::Ptr& arg, bool force) { if (mode == GM_Inventory && mAllowed == GW_None) return; if (mGuiModes.empty() || mGuiModes.back() != mode) { // If this mode already exists somewhere in the stack, just bring it to the front. if (std::find(mGuiModes.begin(), mGuiModes.end(), mode) != mGuiModes.end()) { mGuiModes.erase(std::find(mGuiModes.begin(), mGuiModes.end(), mode)); } if (!mGuiModes.empty()) { mKeyboardNavigation->saveFocus(mGuiModes.back()); mGuiModeStates[mGuiModes.back()].update(false); } mGuiModes.push_back(mode); mGuiModeStates[mode].update(true); } 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); } uint32_t WindowManager::getCullMask() { return mViewer->getCamera()->getCullMask(); } void WindowManager::popGuiMode(bool forceExit) { if (mDragAndDrop && mDragAndDrop->mIsOnDragAndDrop) { mDragAndDrop->finish(); } 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); if (containsMode(mode)) { mGuiModes.pop_back(); mGuiModeStates[mode].update(false); MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); } } if (!mGuiModes.empty()) { const GuiMode mode = mGuiModes.back(); mGuiModeStates[mode].update(true); mKeyboardNavigation->restoreFocus(mode); } updateVisible(); // To make sure that console window get focus again if (mConsole && mConsole->isVisible()) mConsole->onOpen(); } void WindowManager::removeGuiMode(GuiMode mode) { if (!mGuiModes.empty() && mGuiModes.back() == mode) { popGuiMode(); return; } std::vector::iterator it = mGuiModes.begin(); while (it != mGuiModes.end()) { if (*it == mode) it = mGuiModes.erase(it); else ++it; } updateVisible(); MWBase::Environment::get().getLuaManager()->uiModeChanged(MWWorld::Ptr()); } void WindowManager::goToJail(int days) { pushGuiMode(MWGui::GM_Jail); mJailScreen->goToJail(days); } void WindowManager::setSelectedSpell(const ESM::RefId& spellId, int successChancePercent) { mSelectedSpell = spellId; mSelectedEnchantItem = MWWorld::Ptr(); mHud->setSelectedSpell(spellId, successChancePercent); const ESM::Spell* spell = mStore->get().find(spellId); mSpellWindow->setTitle(spell->mName); } void WindowManager::setSelectedEnchantItem(const MWWorld::Ptr& item) { mSelectedEnchantItem = item; mSelectedSpell = ESM::RefId(); const ESM::Enchantment* ench = mStore->get().find(item.getClass().getEnchantment(item)); 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 { return mSelectedEnchantItem; } void WindowManager::setSelectedWeapon(const MWWorld::Ptr& item) { mSelectedWeapon = item; int durabilityPercent = 100; if (item.getClass().hasItemHealth(item)) { durabilityPercent = static_cast(item.getClass().getItemNormalizedHealth(item) * 100); } mHud->setSelectedWeapon(item, durabilityPercent); mInventoryWindow->setTitle(item.getClass().getName(item)); } const MWWorld::Ptr& WindowManager::getSelectedWeapon() const { return mSelectedWeapon; } void WindowManager::unsetSelectedSpell() { 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); mSpellWindow->setTitle("#{Interface:None}"); } void WindowManager::unsetSelectedWeapon() { mSelectedWeapon = MWWorld::Ptr(); mHud->unsetSelectedWeapon(); mInventoryWindow->setTitle("#{sSkillHandtohand}"); } 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) { const MyGUI::IntPoint& pos = MyGUI::InputManager::getInstance().getMousePosition(); x = static_cast(pos.left); y = static_cast(pos.top); const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); x /= viewSize.width; y /= viewSize.height; } bool WindowManager::getWorldMouseOver() { return mHud->getWorldMouseOver(); } float WindowManager::getScalingFactor() const { return mScalingFactor; } void WindowManager::executeInConsole(const std::filesystem::path& 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::PostProcessorHud* WindowManager::getPostProcessorHud() { return mPostProcessorHud; } void WindowManager::useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions) { if (mInventoryWindow) mInventoryWindow->useItem(item, bypassBeastRestrictions); } bool WindowManager::isAllowed(GuiWindow wnd) const { return (mAllowed & wnd) != 0; } void WindowManager::allow(GuiWindow wnd) { mAllowed = (GuiWindow)(mAllowed | wnd); if (wnd & GW_Inventory) { mBookWindow->setInventoryAllowed(true); mScrollWindow->setInventoryAllowed(true); } updateVisible(); } void WindowManager::disallowAll() { mAllowed = GW_None; mRestAllowed = false; mBookWindow->setInventoryAllowed(false); mScrollWindow->setInventoryAllowed(false); updateVisible(); } void WindowManager::toggleVisible(GuiWindow wnd) { if (getMode() != GM_Inventory) return; if (Settings::SettingValue* const hidden = findHiddenSetting(wnd)) hidden->set(!hidden->get()); mShown = (GuiWindow)(mShown ^ wnd); updateVisible(); } void WindowManager::forceHide(GuiWindow wnd) { mForceHidden = (GuiWindow)(mForceHidden | wnd); updateVisible(); } void WindowManager::unsetForceHide(GuiWindow wnd) { mForceHidden = (GuiWindow)(mForceHidden & ~wnd); updateVisible(); } bool WindowManager::isGuiMode() const { return !mGuiModes.empty() || isConsoleMode() || isPostProcessorHudVisible() || isInteractiveMessageBoxActive(); } bool WindowManager::isConsoleMode() const { 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()) return GM_None; return mGuiModes.back(); } void WindowManager::disallowMouse() { mInputBlocker->setVisible(true); } void WindowManager::allowMouse() { mInputBlocker->setVisible(!isGuiMode()); } void WindowManager::notifyInputActionBound() { mSettingsWindow->updateControlsBox(); allowMouse(); } bool WindowManager::containsMode(GuiMode mode) const { if (mGuiModes.empty()) return false; return std::find(mGuiModes.begin(), mGuiModes.end(), mode) != mGuiModes.end(); } void WindowManager::showCrosshair(bool show) { if (mHud) mHud->setCrosshairVisible(show && Settings::hud().mCrosshair); } void WindowManager::updateActivatedQuickKey() { mQuickKeysMenu->updateActivatedQuickKey(); } void WindowManager::activateQuickKey(int index) { mQuickKeysMenu->activateQuickKey(index); } bool WindowManager::setHudVisibility(bool show) { 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(MWWorld::Globals::sCharGenState) == -1) mRestAllowed = true; return mRestAllowed; } bool WindowManager::getPlayerSleeping() { return mWaitDialog->getSleeping(); } void WindowManager::wakeUpPlayer() { mWaitDialog->wakeUp(); } void WindowManager::addVisitedLocation(const std::string& name, int x, int y) { mMap->addVisitedLocation(name, x, y); } const Translation::Storage& WindowManager::getTranslationDataStorage() const { return mTranslationDataStorage; } void WindowManager::changePointer(const std::string& name) { MyGUI::PointerManager::getInstance().setPointer(name); onCursorChange(name); } void WindowManager::showSoulgemDialog(MWWorld::Ptr item) { mSoulgemDialog->show(item); updateVisible(); } void WindowManager::updatePlayer() { mInventoryWindow->updatePlayer(); const MWWorld::Ptr player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { setWerewolfOverlay(true); forceHide((GuiWindow)(MWGui::GW_Inventory | MWGui::GW_Magic)); } } // Remove this wrapper once onKeyFocusChanged call is rendered unnecessary void WindowManager::setKeyFocusWidget(MyGUI::Widget* widget) { MyGUI::InputManager::getInstance().setKeyFocusWidget(widget); onKeyFocusChanged(widget); } void WindowManager::onKeyFocusChanged(MyGUI::Widget* widget) { 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) { mHud->setEnemy(enemy); } std::size_t WindowManager::getMessagesCount() const { std::size_t count = 0; if (mMessageBoxManager) count = mMessageBoxManager->getMessagesCount(); return count; } Loading::Listener* WindowManager::getLoadingScreen() { return mLoadingScreen; } bool WindowManager::getCursorVisible() { return mCursorVisible && mCursorActive; } void WindowManager::trackWindow(Layout* layout, const WindowSettingValues& settings) { MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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); WindowBase::clampWindowCoordinates(window); mTrackedWindows.emplace(window, settings); } void WindowManager::toggleMaximized(Layout* layout) { MyGUI::Window* window = layout->mMainWidget->castType(); const auto it = mTrackedWindows.find(window); if (it == mTrackedWindows.end()) return; const WindowSettingValues& settings = it->second; const WindowRectSettingValues& rect = settings.mIsMaximized ? settings.mRegular : settings.mMaximized; MyGUI::IntSize viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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.mIsMaximized.set(!settings.mIsMaximized.get()); } void WindowManager::onWindowChangeCoord(MyGUI::Window* window) { 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(); 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 (const auto& window : mWindows) { window->clear(); window->setDisabledByLua(false); } if (mLocalMapRender) mLocalMapRender->clear(); mMessageBoxManager->clear(); mToolTips->clear(); mSelectedSpell = ESM::RefId(); mCustomMarkers.clear(); mForceHidden = GW_None; mRestAllowed = true; while (!mGuiModes.empty()) popGuiMode(); updateVisible(); } void WindowManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { mMap->write(writer, progress); mQuickKeysMenu->write(writer); if (!mSelectedSpell.empty()) { writer.startRecord(ESM::REC_ASPL); writer.writeHNRefId("ID__", mSelectedSpell); writer.endRecord(ESM::REC_ASPL); } for (CustomMarkerCollection::ContainerType::const_iterator it = mCustomMarkers.begin(); it != mCustomMarkers.end(); ++it) { writer.startRecord(ESM::REC_MARK); it->second.save(writer); writer.endRecord(ESM::REC_MARK); } } void WindowManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GMAP) mMap->readRecord(reader, type); else if (type == ESM::REC_KEYS) mQuickKeysMenu->readRecord(reader, type); else if (type == ESM::REC_ASPL) { reader.getSubNameIs("ID__"); ESM::RefId spell = reader.getRefId(); if (mStore->get().search(spell)) mSelectedSpell = spell; } else if (type == ESM::REC_MARK) { ESM::CustomMarker marker; marker.load(reader); mCustomMarkers.addMarker(marker, false); } } int WindowManager::countSavedGameRecords() const { return 1 // Global map + 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))); } void WindowManager::playVideo(std::string_view name, bool allowSkipping, bool overrideSounds) { mVideoWidget->playVideo("video\\" + std::string{ name }); mVideoWidget->eventKeyButtonPressed.clear(); mVideoBackground->eventKeyButtonPressed.clear(); if (allowSkipping) { mVideoWidget->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); mVideoBackground->eventKeyButtonPressed += MyGUI::newDelegate(this, &WindowManager::onVideoKeyPressed); } enableScene(false); MyGUI::IntSize screenSize = MyGUI::RenderManager::getInstance().getViewSize(); sizeVideo(screenSize.width, screenSize.height); MyGUI::Widget* oldKeyFocus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); setKeyFocusWidget(mVideoWidget); mVideoBackground->setVisible(true); bool cursorWasVisible = mCursorVisible; setCursorVisible(false); 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()); while (mVideoWidget->update() && !MWBase::Environment::get().getStateManager()->hasQuitRequest()) { const double dt = std::chrono::duration_cast>(frameRateLimiter.getLastFrameDuration()) .count(); MWBase::Environment::get().getInputManager()->update(dt, true, false); if (!mWindowVisible) { mVideoWidget->pause(); std::this_thread::sleep_for(std::chrono::milliseconds(5)); } else { if (mVideoWidget->isPaused()) mVideoWidget->resume(); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); } // at the time this function is called we are in the middle of a frame, // so out of order calls are necessary to get a correct frameNumber for the next frame. // refer to the advance() and frame() order in Engine::go() mViewer->advance(mViewer->getFrameStamp()->getSimulationTime()); frameRateLimiter.limit(); } mVideoWidget->stop(); MWBase::Environment::get().getSoundManager()->resumeSounds(MWSound::VideoPlayback); setKeyFocusWidget(oldKeyFocus); setCursorVisible(cursorWasVisible); // Restore normal rendering updateVisible(); mVideoBackground->setVisible(false); } void WindowManager::sizeVideo(int screenWidth, int screenHeight) { // Use black bars to correct aspect ratio mVideoBackground->setSize(screenWidth, screenHeight); mVideoWidget->autoResize(Settings::gui().mStretchMenuBackground); } void WindowManager::exitCurrentModal() { if (!mCurrentModals.empty()) { WindowModal* window = mCurrentModals.back(); if (!window->exit()) return; window->setVisible(false); } } void WindowManager::addCurrentModal(WindowModal* input) { if (mCurrentModals.empty()) mKeyboardNavigation->saveFocus(getMode()); mCurrentModals.push_back(input); mKeyboardNavigation->restoreFocus(-1); mKeyboardNavigation->setModalWindow(input->mMainWidget); mKeyboardNavigation->setDefaultFocus(input->mMainWidget, input->getDefaultKeyFocus()); } void WindowManager::removeCurrentModal(WindowModal* input) { if (!mCurrentModals.empty()) { if (input == mCurrentModals.back()) { mCurrentModals.pop_back(); mKeyboardNavigation->saveFocus(-1); } else { auto found = std::find(mCurrentModals.begin(), mCurrentModals.end(), input); if (found != mCurrentModals.end()) mCurrentModals.erase(found); else Log(Debug::Warning) << "Warning: can't find modal window " << input; } } if (mCurrentModals.empty()) { mKeyboardNavigation->setModalWindow(nullptr); mKeyboardNavigation->restoreFocus(getMode()); } else mKeyboardNavigation->setModalWindow(mCurrentModals.back()->mMainWidget); } void WindowManager::onVideoKeyPressed(MyGUI::Widget* _sender, MyGUI::KeyCode _key, MyGUI::Char _char) { if (_key == MyGUI::KeyCode::Escape) mVideoWidget->stop(); } void WindowManager::updatePinnedWindows() { mInventoryWindow->setPinned(Settings::windows().mInventoryPin); if (Settings::windows().mInventoryHidden) mShown = (GuiWindow)(mShown ^ GW_Inventory); mMap->setPinned(Settings::windows().mMapPin); if (Settings::windows().mMapHidden) mShown = (GuiWindow)(mShown ^ GW_Map); mSpellWindow->setPinned(Settings::windows().mSpellsPin); if (Settings::windows().mSpellsHidden) mShown = (GuiWindow)(mShown ^ GW_Magic); mStatsWindow->setPinned(Settings::windows().mStatsPin); if (Settings::windows().mStatsHidden) mShown = (GuiWindow)(mShown ^ GW_Stats); } void WindowManager::pinWindow(GuiWindow window) { 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; } updateVisible(); } void WindowManager::fadeScreenIn(const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeOut(time, delay); } void WindowManager::fadeScreenOut(const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeIn(time, delay); } void WindowManager::fadeScreenTo(const int percent, const float time, bool clearQueue, float delay) { if (clearQueue) mScreenFader->clearQueue(); mScreenFader->fadeTo(percent, time, delay); } void WindowManager::setBlindness(const int percent) { mBlindnessFader->notifyAlphaChanged(percent / 100.f); } void WindowManager::activateHitOverlay(bool interrupt) { if (!Settings::gui().mHitFader) return; if (!interrupt && !mHitFader->isEmpty()) return; mHitFader->clearQueue(); mHitFader->fadeTo(100, 0.0f); mHitFader->fadeTo(0, 0.5f); } void WindowManager::setWerewolfOverlay(bool set) { if (!Settings::gui().mWerewolfOverlay) return; if (mWerewolfFader) mWerewolfFader->notifyAlphaChanged(set ? 1.0f : 0.0f); } 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(std::string_view _type, std::string& _data) { if (_type != "Text") return; char* text = nullptr; text = SDL_GetClipboardText(); if (text) _data = MyGUI::TextIterator::toTagsString(text); SDL_free(text); } void WindowManager::toggleConsole() { bool visible = mConsole->isVisible(); if (!visible && !mGuiModes.empty()) mKeyboardNavigation->saveFocus(mGuiModes.back()); mConsole->setVisible(!visible); if (visible && !mGuiModes.empty()) mKeyboardNavigation->restoreFocus(mGuiModes.back()); updateVisible(); } void WindowManager::toggleDebugWindow() { mDebugWindow->setVisible(!mDebugWindow->isVisible()); } 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) { if (!isGuiMode()) mSpellWindow->cycle(next); } void WindowManager::cycleWeapon(bool next) { if (!isGuiMode()) mInventoryWindow->cycle(next); } 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::NoEnvNoScaling); } void WindowManager::updateSpellWindow() { if (mSpellWindow) mSpellWindow->updateSpells(); } void WindowManager::setConsoleSelectedObject(const MWWorld::Ptr& object) { mConsole->setSelectedObject(object); } MWWorld::Ptr WindowManager::getConsoleSelectedObject() const { return mConsole->getSelectedObject(); } void WindowManager::printToConsole(const std::string& msg, std::string_view color) { mConsole->print(msg, color); } void WindowManager::setConsoleMode(std::string_view mode) { mConsole->setConsoleMode(mode); } const std::string& WindowManager::getConsoleMode() { return mConsole->getConsoleMode(); } void WindowManager::createCursors() { MyGUI::ResourceManager::EnumeratorPtr enumerator = MyGUI::ResourceManager::getInstance().getEnumerator(); while (enumerator.next()) { MyGUI::IResource* resource = enumerator.current().second; ResourceImageSetPointerFix* imgSetPointer = resource->castType(false); if (!imgSetPointer) continue; const VFS::Path::Normalized path(imgSetPointer->getImageSet()->getIndexInfo(0, 0).texture); osg::ref_ptr image = mResourceSystem->getImageManager()->getImage(path); if (image.valid()) { // 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, pointerSize.width, pointerSize.height); } } } void WindowManager::createTextures() { { 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) { *(data++) = 255; *(data++) = 255; *(data++) = 255; } tex->unlock(); } { 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) { *(data++) = 0; *(data++) = 0; *(data++) = 0; } tex->unlock(); } { MyGUI::ITexture* tex = MyGUI::RenderManager::getInstance().createTexture("transparent"); tex->createManual(8, 8, MyGUI::TextureUsage::Write, MyGUI::PixelFormat::R8G8B8A8); setMenuTransparency(Settings::gui().mMenuTransparency); } } void WindowManager::setMenuTransparency(float value) { 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) { *(data++) = 255; *(data++) = 255; *(data++) = 255; *(data++) = static_cast(value * 255); } tex->unlock(); } void WindowManager::addCell(MWWorld::CellStore* cell) { mLocalMapRender->addCell(cell); } void WindowManager::removeCell(MWWorld::CellStore* cell) { mLocalMapRender->removeCell(cell); } void WindowManager::writeFog(MWWorld::CellStore* cell) { mLocalMapRender->saveFogOfWar(cell); } const MWGui::TextColours& WindowManager::getTextColours() { return mTextColours; } bool WindowManager::injectKeyPress(MyGUI::KeyCode key, unsigned int text, bool repeat) { if (!mKeyboardNavigation->injectKeyPress(key, text, repeat)) { MyGUI::Widget* focus = MyGUI::InputManager::getInstance().getKeyFocusWidget(); bool widgetActive = MyGUI::InputManager::getInstance().injectKeyPress(key, text); if (!widgetActive || !focus) return false; // FIXME: MyGUI doesn't allow widgets to state if a given key was actually used, so make a guess if (focus->getTypeName().find("Button") != std::string::npos) { 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; } } return false; } else return true; } bool WindowManager::injectKeyRelease(MyGUI::KeyCode key) { return MyGUI::InputManager::getInstance().injectKeyRelease(key); } void WindowManager::GuiModeState::update(bool visible) { for (const auto& window : mWindows) window->setVisible(visible); } void WindowManager::watchActor(const MWWorld::Ptr& ptr) { mStatsWatcher->watchActor(ptr); } MWWorld::Ptr WindowManager::getWatchedActor() const { 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; } } openmw-openmw-0.49.0/apps/openmw/mwgui/windowmanagerimp.hpp000066400000000000000000000523511503074453300241070ustar00rootroot00000000000000#ifndef MWGUI_WINDOWMANAGERIMP_H #define MWGUI_WINDOWMANAGERIMP_H /** This class owns and controls all the MW specific windows in the GUI. It can enable/disable Gui mode, and is responsible for sending 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 Widget; class Window; class UString; class ImageBox; } namespace MWWorld { class Cell; class ESMStore; } namespace Compiler { class Extensions; } namespace Translation { class Storage; } namespace osg { class Group; } namespace osgViewer { class Viewer; } namespace Resource { class ResourceSystem; } namespace SceneUtil { class WorkQueue; } namespace Gui { class FontLoader; } namespace MWGui { 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; 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, bool exportFonts, const std::string& versionDescription, bool useShaders, Files::ConfigurationManager& cfgMgr); virtual ~WindowManager(); /// Set the ESMStore to use for retrieving of GUI-related strings. void setStore(const MWWorld::ESMStore& store); void initUI(); 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(std::string_view name, bool allowSkipping, bool overrideSounds = true) 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 forceExit = false) override; void removeGuiMode(GuiMode mode) 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; bool isPostProcessorHudVisible() const override; bool isSettingsWindowVisible() const override; bool isInteractiveMessageBoxActive() 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; MWGui::PostProcessorHud* getPostProcessorHud() 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; 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; /// 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 changeCell(const MWWorld::CellStore* cell) override; ///< change the active cell void setFocusObject(const MWWorld::Ptr& focus) override; void setFocusObjectScreenCoords(float x, float y) override; void getMousePosition(int& x, int& y) override; void getMousePosition(float& x, float& y) override; void setDragDrop(bool dragDrop) override; bool getWorldMouseOver() override; float getScalingFactor() const override; bool toggleFogOfWar() override; bool toggleFullHelp() override; ///< show extra info in item tooltips (owner, script) bool getFullHelp() const override; /// sets the visibility of the drowning bar void setDrowningBarVisibility(bool visible) 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; /// 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; 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 updateConsoleObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) override; void showCrosshair(bool show) override; /// Turn visibility of HUD on or off bool setHudVisibility(bool show) override; bool isHudVisible() const override { return mHudEnabled; } void disallowMouse() override; void allowMouse() override; void notifyInputActionBound() override; void addVisitedLocation(const std::string& name, int x, int y) override; /// Hides dialog and schedules dialog to be deleted. void removeDialog(std::unique_ptr&& dialog) override; /// Gracefully attempts to exit the topmost GUI mode void exitCurrentGuiMode() 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; int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed ///< (->MessageBoxmanager->InteractiveMessageBox) void update(float duration); /** * 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; void processChangedSettings(const Settings::CategorySettingVector& changed) override; void windowVisibilityChange(bool visible) override; void windowResized(int x, int y) override; void windowClosed() override; bool isWindowVisible() const override; void watchActor(const MWWorld::Ptr& ptr) override; MWWorld::Ptr getWatchedActor() const override; void executeInConsole(const std::filesystem::path& path) override; void enableRest() override { mRestAllowed = true; } bool getRestEnabled() override; bool getJournalAllowed() override { return (mAllowed & GW_Magic) != 0; } bool getPlayerSleeping() override; void wakeUpPlayer() override; void updatePlayer() override; void showSoulgemDialog(MWWorld::Ptr item) override; void changePointer(const std::string& name) override; void setEnemy(const MWWorld::Ptr& enemy) override; std::size_t getMessagesCount() const override; const Translation::Storage& getTranslationDataStorage() const override; bool getCursorVisible() override; /// Call when mouse cursor or buttons are used. void setCursorActive(bool active) override; /// Clear all savegame-specific data void clear() override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; int countSavedGameRecords() const override; /// Does the current stack of GUI-windows permit saving? bool isSavingAllowed() const override; /// Send exit command to active Modal window **/ void exitCurrentModal() override; /// 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 { GuiModeState(WindowBase* window) { mWindows.push_back(window); } GuiModeState(const std::vector& windows) : mWindows(windows) { } GuiModeState() {} void update(bool visible); 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::unique_ptr mCursorManager; std::vector> mGarbageDialogs; void cleanupGarbage(); GuiWindow mShown; // Currently shown windows in inventory mode GuiWindow mForceHidden; // Hidden windows (overrides mShown) /* 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 updateVisible(); // Update visibility of all windows based on mode, shown and allowed settings void updateMap(); ToUTF8::FromType mEncoding; std::string mVersionDescription; bool mWindowVisible; MWGui::TextColours mTextColours; std::unique_ptr mKeyboardNavigation; std::unique_ptr mVideoWrapper; float mScalingFactor; struct ScheduledMessageBox { std::string mMessage; MWGui::ShowInDialogueMode mShowInDialogueMode; ScheduledMessageBox(std::string&& message, MWGui::ShowInDialogueMode showInDialogueMode) : mMessage(std::move(message)) , mShowInDialogueMode(showInDialogueMode) { } }; 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); 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); void sizeVideo(int screenWidth, int screenHeight); 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 updatePinnedWindows(); 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 openmw-openmw-0.49.0/apps/openmw/mwgui/windowpinnablebase.cpp000066400000000000000000000021471503074453300244030ustar00rootroot00000000000000#include "windowpinnablebase.hpp" #include "exposedwindow.hpp" namespace MWGui { WindowPinnableBase::WindowPinnableBase(const std::string& parLayout) : WindowBase(parLayout) , mPinned(false) { Window* window = mMainWidget->castType(); mPinButton = window->getSkinWidget("Button"); mPinButton->eventMouseButtonPressed += MyGUI::newDelegate(this, &WindowPinnableBase::onPinButtonPressed); } void WindowPinnableBase::onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id) { if (id != MyGUI::MouseButton::Left) return; mPinned = !mPinned; if (mPinned) mPinButton->changeWidgetSkin("PinDown"); else mPinButton->changeWidgetSkin("PinUp"); onPinToggled(); } void WindowPinnableBase::setPinned(bool pinned) { if (pinned != mPinned) onPinButtonPressed(mPinButton, 0, 0, MyGUI::MouseButton::Left); } void WindowPinnableBase::setPinButtonVisible(bool visible) { mPinButton->setVisible(visible); } } openmw-openmw-0.49.0/apps/openmw/mwgui/windowpinnablebase.hpp000066400000000000000000000011511503074453300244020ustar00rootroot00000000000000#ifndef MWGUI_WINDOW_PINNABLE_BASE_H #define MWGUI_WINDOW_PINNABLE_BASE_H #include "windowbase.hpp" namespace MWGui { class WindowPinnableBase : public WindowBase { public: WindowPinnableBase(const std::string& parLayout); bool pinned() { return mPinned; } void setPinned(bool pinned); void setPinButtonVisible(bool visible); private: void onPinButtonPressed(MyGUI::Widget* _sender, int left, int top, MyGUI::MouseButton id); protected: virtual void onPinToggled() = 0; MyGUI::Widget* mPinButton; bool mPinned; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/000077500000000000000000000000001503074453300203735ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwinput/actionmanager.cpp000066400000000000000000000244511503074453300237150ustar00rootroot00000000000000#include "actionmanager.hpp" #include #include #include #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/class.hpp" #include "../mwworld/globals.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" namespace MWInput { ActionManager::ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler) : mBindingsManager(bindingsManager) , mViewer(std::move(viewer)) , mScreenCaptureHandler(std::move(screenCaptureHandler)) , mTimeIdle(0.f) { } void ActionManager::update(float dt) { 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 mTimeIdle += dt; } void ActionManager::resetIdleTime() { mTimeIdle = 0.f; } void ActionManager::executeAction(int action) { 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_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; } } bool ActionManager::checkAllowedToUseItems() const { MWWorld::Ptr player = MWMechanics::getPlayer(); if (player.getClass().getNpcStats(player).isWerewolf()) { // Cannot use items or spells while in werewolf form MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return false; } return true; } void ActionManager::screenshot() { mScreenCaptureHandler->setFramesToCapture(1); mScreenCaptureHandler->captureNextFrame(*mViewer); } void ActionManager::toggleMainMenu() { if (MyGUI::InputManager::getInstance().isModalAny()) { MWBase::Environment::get().getWindowManager()->exitCurrentModal(); return; } if (MWBase::Environment::get().getWindowManager()->isConsoleMode()) { MWBase::Environment::get().getWindowManager()->toggleConsole(); return; } if (MWBase::Environment::get().getWindowManager()->isPostProcessorHudVisible()) { MWBase::Environment::get().getWindowManager()->togglePostProcessorHud(); return; } 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::quickLoad() { if (!MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getStateManager()->quickLoad(); } void ActionManager::quickSave() { if (!MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getStateManager()->quickSave(); } 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::toggleConsole() { if (MyGUI::InputManager::getInstance().isModalAny()) return; MWBase::Environment::get().getWindowManager()->toggleConsole(); } void ActionManager::quickKey(int index) { 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(MWWorld::Globals::sCharGenState) != -1) return; if (!MWBase::Environment::get().getWindowManager()->isGuiMode()) MWBase::Environment::get().getWindowManager()->activateQuickKey(index); } void ActionManager::activate() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); if (!SDL_IsTextInputActive() && !mBindingsManager->isLeftOrRightButton(A_Activate, joystickUsed)) MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Return, 0, false); } else if (MWBase::Environment::get().getInputManager()->getControlSwitch("playercontrols")) { MWWorld::Player& player = MWBase::Environment::get().getWorld()->getPlayer(); player.activate(); } } bool ActionManager::isSneaking() const { const MWBase::Environment& env = MWBase::Environment::get(); return env.getMechanicsManager()->isSneaking(env.getWorld()->getPlayer().getPlayer()); } void ActionManager::handleGuiArrowKey(int action) { bool joystickUsed = MWBase::Environment::get().getInputManager()->joystickLastUsed(); // This is currently keyboard-specific code // TODO: see if GUI controls can be refactored into a single function if (joystickUsed) return; if (SDL_IsTextInputActive()) return; if (mBindingsManager->isLeftOrRightButton(action, joystickUsed)) return; 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; } MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); } } openmw-openmw-0.49.0/apps/openmw/mwinput/actionmanager.hpp000066400000000000000000000022641503074453300237200ustar00rootroot00000000000000#ifndef MWINPUT_ACTIONMANAGER_H #define MWINPUT_ACTIONMANAGER_H #include #include namespace osgViewer { class Viewer; class ScreenCaptureHandler; } namespace MWInput { class BindingsManager; class ActionManager { public: ActionManager(BindingsManager* bindingsManager, osg::ref_ptr viewer, osg::ref_ptr screenCaptureHandler); void update(float dt); void executeAction(int action); bool checkAllowedToUseItems() const; void toggleMainMenu(); void toggleConsole(); void screenshot(); void activate(); void rest(); void quickLoad(); void quickSave(); void quickKey(int index); void resetIdleTime(); float getIdleTime() const { return mTimeIdle; } bool isSneaking() const; private: void handleGuiArrowKey(int action); BindingsManager* mBindingsManager; osg::ref_ptr mViewer; osg::ref_ptr mScreenCaptureHandler; float mTimeIdle; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/actions.hpp000066400000000000000000000034061503074453300225470ustar00rootroot00000000000000#ifndef MWINPUT_ACTIONS_H #define MWINPUT_ACTIONS_H namespace MWInput { enum Actions { // 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 openmw-openmw-0.49.0/apps/openmw/mwinput/bindingsmanager.cpp000066400000000000000000000754171503074453300242450ustar00rootroot00000000000000#include "bindingsmanager.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "actions.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 void clearAllKeyBindings(ICS::InputControlSystem* inputBinder, ICS::Control* control) { // right now we don't really need multiple bindings for the same action, so remove all others first if (inputBinder->getKeyBinding(control, ICS::Control::INCREASE) != SDL_SCANCODE_UNKNOWN) 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) inputBinder->removeMouseWheelBinding(inputBinder->getMouseWheelBinding(control, ICS::Control::INCREASE)); } void clearAllControllerBindings(ICS::InputControlSystem* inputBinder, ICS::Control* control) { // 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)); } class InputControlSystem : public ICS::InputControlSystem { public: InputControlSystem(const std::filesystem::path& bindingsFile) : ICS::InputControlSystem(Files::pathToUnicodeString(bindingsFile), true, nullptr, nullptr, A_Last) { } }; class BindingsListener : public ICS::ChannelListener, public ICS::DetectingBindingListener { public: BindingsListener(ICS::InputControlSystem* inputBinder, BindingsManager* bindingsManager) : mInputBinder(inputBinder) , mBindingsManager(bindingsManager) , mDetectingKeyboard(false) { } virtual ~BindingsListener() = default; void channelChanged(ICS::Channel* channel, float currentValue, float previousValue) override { int action = channel->getNumber(); mBindingsManager->actionValueChanged(action, currentValue, previousValue); } 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) { // Stop binding if esc pressed mInputBinder->cancelDetectingBindingState(); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); return; } // Disallow binding reserved keys if (key == SDL_SCANCODE_F3 || key == SDL_SCANCODE_F4 || key == SDL_SCANCODE_F10) return; #ifndef __APPLE__ // Disallow binding Windows/Meta keys if (key == SDL_SCANCODE_LGUI || key == SDL_SCANCODE_RGUI) return; #endif if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::keyBindingDetected(ICS, control, key, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } 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 { if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::mouseButtonBindingDetected(ICS, control, button, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void mouseWheelBindingDetected(ICS::InputControlSystem* ICS, ICS::Control* control, ICS::InputControlSystem::MouseWheelClick click, ICS::Control::ControlChangingDirection direction) override { if (!mDetectingKeyboard) return; clearAllKeyBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::mouseWheelBindingDetected(ICS, control, click, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void joystickAxisBindingDetected(ICS::InputControlSystem* ICS, int deviceID, ICS::Control* control, int axis, ICS::Control::ControlChangingDirection direction) override { // 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->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 { if (mDetectingKeyboard) return; clearAllControllerBindings(mInputBinder, control); control->setInitialValue(0.0f); ICS::DetectingBindingListener::joystickButtonBindingDetected(ICS, deviceID, control, button, direction); MWBase::Environment::get().getWindowManager()->notifyInputActionBound(); } void setDetectingKeyboard(bool detecting) { mDetectingKeyboard = detecting; } private: ICS::InputControlSystem* mInputBinder; BindingsManager* mBindingsManager; bool mDetectingKeyboard; }; BindingsManager::BindingsManager(const std::filesystem::path& userFile, bool userFileExists) : mUserFile(userFile) , mDragDrop(false) { const auto file = userFileExists ? userFile : std::filesystem::path(); mInputBinder = std::make_unique(file); mListener = std::make_unique(mInputBinder.get(), this); mInputBinder->setDetectingBindingListener(mListener.get()); loadKeyDefaults(); loadControllerDefaults(); for (int i = 0; i < A_Last; ++i) { mInputBinder->getChannel(i)->addListener(mListener.get()); } } void BindingsManager::setDragDrop(bool dragDrop) { mDragDrop = dragDrop; } BindingsManager::~BindingsManager() { const std::string newFileName = Files::pathToUnicodeString(mUserFile) + ".new"; try { if (mInputBinder->save(newFileName)) { std::filesystem::rename(Files::pathFromUnicodeString(newFileName), mUserFile); Log(Debug::Info) << "Saved input bindings: " << mUserFile; } else { Log(Debug::Error) << "Failed to save input bindings to " << newFileName; } } catch (const std::exception& e) { Log(Debug::Error) << "Failed to save input bindings to " << newFileName << ": " << e.what(); } } void BindingsManager::update(float dt) { // update values of channels (as a result of pressed keys) mInputBinder->update(dt); } bool BindingsManager::isLeftOrRightButton(int action, bool joystick) const { 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); if (joystick && (buttonBinding == 0 || buttonBinding == 1)) return true; return false; } 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 }; for (int pc : playerChannels) { mInputBinder->getChannel(pc)->setEnabled(enabled); } } void BindingsManager::setJoystickDeadZone(float deadZone) { mInputBinder->setJoystickDeadZone(deadZone); } float BindingsManager::getActionValue(int id) const { return mInputBinder->getChannel(id)->getValue(); } bool BindingsManager::actionIsActive(int id) const { return getActionValue(id) == 1.0; } 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 defaultKeyBindings[A_Activate] = SDL_SCANCODE_SPACE; defaultKeyBindings[A_MoveBackward] = SDL_SCANCODE_S; defaultKeyBindings[A_MoveForward] = SDL_SCANCODE_W; defaultKeyBindings[A_MoveLeft] = SDL_SCANCODE_A; defaultKeyBindings[A_MoveRight] = SDL_SCANCODE_D; defaultKeyBindings[A_ToggleWeapon] = SDL_SCANCODE_F; defaultKeyBindings[A_ToggleSpell] = SDL_SCANCODE_R; defaultKeyBindings[A_CycleSpellLeft] = SDL_SCANCODE_MINUS; defaultKeyBindings[A_CycleSpellRight] = SDL_SCANCODE_EQUALS; defaultKeyBindings[A_CycleWeaponLeft] = SDL_SCANCODE_LEFTBRACKET; defaultKeyBindings[A_CycleWeaponRight] = SDL_SCANCODE_RIGHTBRACKET; defaultKeyBindings[A_QuickKeysMenu] = SDL_SCANCODE_F1; defaultKeyBindings[A_Console] = SDL_SCANCODE_GRAVE; defaultKeyBindings[A_Run] = SDL_SCANCODE_LSHIFT; defaultKeyBindings[A_Sneak] = SDL_SCANCODE_LCTRL; defaultKeyBindings[A_AutoMove] = SDL_SCANCODE_Q; defaultKeyBindings[A_Jump] = SDL_SCANCODE_E; defaultKeyBindings[A_Journal] = SDL_SCANCODE_J; defaultKeyBindings[A_Rest] = SDL_SCANCODE_T; defaultKeyBindings[A_GameMenu] = SDL_SCANCODE_ESCAPE; defaultKeyBindings[A_TogglePOV] = SDL_SCANCODE_TAB; defaultKeyBindings[A_QuickKey1] = SDL_SCANCODE_1; defaultKeyBindings[A_QuickKey2] = SDL_SCANCODE_2; defaultKeyBindings[A_QuickKey3] = SDL_SCANCODE_3; defaultKeyBindings[A_QuickKey4] = SDL_SCANCODE_4; defaultKeyBindings[A_QuickKey5] = SDL_SCANCODE_5; defaultKeyBindings[A_QuickKey6] = SDL_SCANCODE_6; defaultKeyBindings[A_QuickKey7] = SDL_SCANCODE_7; defaultKeyBindings[A_QuickKey8] = SDL_SCANCODE_8; defaultKeyBindings[A_QuickKey9] = SDL_SCANCODE_9; defaultKeyBindings[A_QuickKey10] = SDL_SCANCODE_0; defaultKeyBindings[A_Screenshot] = SDL_SCANCODE_F12; defaultKeyBindings[A_ToggleHUD] = SDL_SCANCODE_F11; defaultKeyBindings[A_ToggleDebug] = SDL_SCANCODE_F10; 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; defaultMouseButtonBindings[A_Use] = SDL_BUTTON_LEFT; std::map defaultMouseWheelBindings; defaultMouseWheelBindings[A_ZoomIn] = ICS::InputControlSystem::MouseWheelClick::UP; defaultMouseWheelBindings[A_ZoomOut] = ICS::InputControlSystem::MouseWheelClick::DOWN; for (int i = 0; i < A_Last; ++i) { ICS::Control* control; bool controlExists = mInputBinder->getChannel(i)->getControlsCount() != 0; if (!controlExists) { control = new ICS::Control(std::to_string(i), false, true, 0, ICS::ICS_MAX, ICS::ICS_MAX); mInputBinder->addControl(control); control->attachChannel(mInputBinder->getChannel(i), ICS::Channel::DIRECT); } else { 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)) { clearAllKeyBindings(mInputBinder.get(), control); if (defaultKeyBindings.find(i) != defaultKeyBindings.end() && (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]))) { control->setInitialValue(0.0f); mInputBinder->addMouseButtonBinding(control, defaultMouseButtonBindings[i], ICS::Control::INCREASE); } else if (defaultMouseWheelBindings.find(i) != defaultMouseWheelBindings.end() && (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)) { 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)) { mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_2, ICS::Control::INCREASE); mInputBinder->addKeyBinding(control, SDL_SCANCODE_KP_8, ICS::Control::DECREASE); } } } } void BindingsManager::loadControllerDefaults(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 defaultButtonBindings; 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_Sneak] = SDL_CONTROLLER_BUTTON_LEFTSTICK; defaultButtonBindings[A_Journal] = SDL_CONTROLLER_BUTTON_LEFTSHOULDER; defaultButtonBindings[A_Rest] = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER; defaultButtonBindings[A_TogglePOV] = SDL_CONTROLLER_BUTTON_RIGHTSTICK; defaultButtonBindings[A_Inventory] = SDL_CONTROLLER_BUTTON_B; defaultButtonBindings[A_GameMenu] = SDL_CONTROLLER_BUTTON_START; defaultButtonBindings[A_QuickSave] = SDL_CONTROLLER_BUTTON_GUIDE; std::map defaultAxisBindings; defaultAxisBindings[A_MoveForwardBackward] = SDL_CONTROLLER_AXIS_LEFTY; defaultAxisBindings[A_MoveLeftRight] = SDL_CONTROLLER_AXIS_LEFTX; defaultAxisBindings[A_LookUpDown] = SDL_CONTROLLER_AXIS_RIGHTY; defaultAxisBindings[A_LookLeftRight] = SDL_CONTROLLER_AXIS_RIGHTX; defaultAxisBindings[A_Use] = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; defaultAxisBindings[A_Jump] = SDL_CONTROLLER_AXIS_TRIGGERLEFT; for (int i = 0; i < A_Last; i++) { ICS::Control* control; bool controlExists = mInputBinder->getChannel(i)->getControlsCount() != 0; if (!controlExists) { float initial; if (defaultAxisBindings.find(i) == defaultAxisBindings.end()) initial = 0.0f; 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); } else { 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)) { clearAllControllerBindings(mInputBinder.get(), control); if (defaultButtonBindings.find(i) != defaultButtonBindings.end() && (force || !mInputBinder->isJoystickButtonBound(sFakeDeviceId, defaultButtonBindings[i]))) { control->setInitialValue(0.0f); mInputBinder->addJoystickButtonBinding( control, sFakeDeviceId, defaultButtonBindings[i], ICS::Control::INCREASE); } 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); } } } } std::string_view BindingsManager::getActionDescription(int action) { switch (action) { case A_Screenshot: return "#{OMWEngine:Screenshot}"; case A_ZoomIn: return "#{OMWEngine:CameraZoomIn}"; case A_ZoomOut: return "#{OMWEngine:CameraZoomOut}"; case A_ToggleHUD: return "#{OMWEngine:ToggleHUD}"; case A_Use: return "#{sUse}"; case A_Activate: return "#{sActivate}"; case A_MoveBackward: return "#{sBack}"; case A_MoveForward: return "#{sForward}"; case A_MoveLeft: return "#{sLeft}"; case A_MoveRight: return "#{sRight}"; case A_ToggleWeapon: return "#{sReady_Weapon}"; case A_ToggleSpell: return "#{sReady_Magic}"; case A_CycleSpellLeft: return "#{sPrevSpell}"; case A_CycleSpellRight: return "#{sNextSpell}"; case A_CycleWeaponLeft: return "#{sPrevWeapon}"; case A_CycleWeaponRight: return "#{sNextWeapon}"; case A_Console: return "#{OMWEngine:ConsoleWindow}"; case A_Run: return "#{sRun}"; case A_Sneak: return "#{sCrouch_Sneak}"; case A_AutoMove: return "#{sAuto_Run}"; case A_Jump: return "#{sJump}"; case A_Journal: return "#{sJournal}"; case A_Rest: return "#{sRestKey}"; case A_Inventory: return "#{sInventory}"; case A_TogglePOV: return "#{sTogglePOVCmd}"; case A_QuickKeysMenu: return "#{sQuickMenu}"; case A_QuickKey1: return "#{sQuick1Cmd}"; case A_QuickKey2: return "#{sQuick2Cmd}"; case A_QuickKey3: return "#{sQuick3Cmd}"; case A_QuickKey4: return "#{sQuick4Cmd}"; case A_QuickKey5: return "#{sQuick5Cmd}"; case A_QuickKey6: return "#{sQuick6Cmd}"; case A_QuickKey7: return "#{sQuick7Cmd}"; case A_QuickKey8: return "#{sQuick8Cmd}"; case A_QuickKey9: return "#{sQuick9Cmd}"; case A_QuickKey10: return "#{sQuick10Cmd}"; case A_AlwaysRun: return "#{sAlways_Run}"; case A_QuickSave: return "#{sQuickSaveCmd}"; case A_QuickLoad: return "#{sQuickLoadCmd}"; case A_TogglePostProcessorHUD: return "#{OMWEngine:TogglePostProcessorHUD}"; default: return {}; // not configurable } } std::string BindingsManager::getActionKeyBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) return "#{Interface:None}"; ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; SDL_Scancode key = mInputBinder->getKeyBinding(c, ICS::Control::INCREASE); unsigned int mouse = mInputBinder->getMouseButtonBinding(c, ICS::Control::INCREASE); ICS::InputControlSystem::MouseWheelClick wheel = mInputBinder->getMouseWheelBinding(c, ICS::Control::INCREASE); if (key != SDL_SCANCODE_UNKNOWN) return MyGUI::TextIterator::toTagsString(mInputBinder->scancodeToString(key)); else if (mouse != ICS_MAX_DEVICE_BUTTONS) return "#{sMouse} " + std::to_string(mouse); else if (wheel != ICS::InputControlSystem::MouseWheelClick::UNASSIGNED) switch (wheel) { case ICS::InputControlSystem::MouseWheelClick::UP: return "Mouse Wheel Up"; case ICS::InputControlSystem::MouseWheelClick::DOWN: return "Mouse Wheel Down"; case ICS::InputControlSystem::MouseWheelClick::RIGHT: return "Mouse Wheel Right"; case ICS::InputControlSystem::MouseWheelClick::LEFT: return "Mouse Wheel Left"; default: return "#{Interface:None}"; } else return "#{Interface:None}"; } std::string BindingsManager::getActionControllerBindingName(int action) { if (mInputBinder->getChannel(action)->getControlsCount() == 0) return "#{Interface:None}"; ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; 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 "#{Interface:None}"; } const std::initializer_list& BindingsManager::getActionKeySorting() { 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; } const std::initializer_list& BindingsManager::getActionControllerSorting() { 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; } void BindingsManager::enableDetectingBindingMode(int action, bool keyboard) { mListener->setDetectingKeyboard(keyboard); ICS::Control* c = mInputBinder->getChannel(action)->getAttachedControls().front().control; mInputBinder->enableDetectingBindingState(c, ICS::Control::INCREASE); } bool BindingsManager::isDetectingBindingState() const { return mInputBinder->detectingBindingState(); } void BindingsManager::mousePressed(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mousePressed(arg, deviceID); } void BindingsManager::mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 deviceID) { mInputBinder->mouseReleased(arg, deviceID); } void BindingsManager::mouseMoved(const SDLUtil::MouseMotionEvent& arg) { mInputBinder->mouseMoved(arg); } void BindingsManager::mouseWheelMoved(const SDL_MouseWheelEvent& arg) { mInputBinder->mouseWheelMoved(arg); } void BindingsManager::keyPressed(const SDL_KeyboardEvent& arg) { mInputBinder->keyPressed(arg); } void BindingsManager::keyReleased(const SDL_KeyboardEvent& arg) { mInputBinder->keyReleased(arg); } void BindingsManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg) { mInputBinder->controllerAdded(deviceID, arg); } void BindingsManager::controllerRemoved(const SDL_ControllerDeviceEvent& arg) { mInputBinder->controllerRemoved(arg); } void BindingsManager::controllerButtonPressed(int deviceID, const SDL_ControllerButtonEvent& arg) { mInputBinder->buttonPressed(deviceID, arg); } void BindingsManager::controllerButtonReleased(int deviceID, const SDL_ControllerButtonEvent& arg) { mInputBinder->buttonReleased(deviceID, arg); } void BindingsManager::controllerAxisMoved(int deviceID, const SDL_ControllerAxisEvent& arg) { mInputBinder->axisMoved(deviceID, arg); } SDL_Scancode BindingsManager::getKeyBinding(int actionId) { 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) { auto manager = MWBase::Environment::get().getInputManager(); manager->resetIdleTime(); if (mDragDrop && action != A_GameMenu && action != A_Inventory) return; if (manager->joystickLastUsed() && manager->getControlSwitch("playercontrols")) { 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 (previousValue <= 0.6 && currentValue > 0.6) manager->executeAction(action); } } openmw-openmw-0.49.0/apps/openmw/mwinput/bindingsmanager.hpp000066400000000000000000000050441503074453300242370ustar00rootroot00000000000000#ifndef MWINPUT_MWBINDINGSMANAGER_H #define MWINPUT_MWBINDINGSMANAGER_H #include #include #include #include #include namespace MWInput { class BindingsListener; class InputControlSystem; class BindingsManager { public: BindingsManager(const std::filesystem::path& userFile, bool userFileExists); virtual ~BindingsManager(); 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); bool isDetectingBindingState() const; void loadKeyDefaults(bool force = false); void loadControllerDefaults(bool force = false); void setDragDrop(bool dragDrop); void update(float dt); void setPlayerControlsEnabled(bool enabled); void setJoystickDeadZone(float deadZone); bool isLeftOrRightButton(int action, bool joystick) const; bool actionIsActive(int id) const; float getActionValue(int id) const; // returns value in range [0, 1] SDL_GameController* getControllerOrNull() const; 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 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); void actionValueChanged(int action, float currentValue, float previousValue); private: void setupSDLKeyMappings(); std::unique_ptr mInputBinder; std::unique_ptr mListener; std::filesystem::path mUserFile; bool mDragDrop; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/controllermanager.cpp000066400000000000000000000404701503074453300246220ustar00rootroot00000000000000#include "controllermanager.hpp" #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 "actions.hpp" #include "bindingsmanager.hpp" #include "mousemanager.hpp" namespace MWInput { ControllerManager::ControllerManager(BindingsManager* bindingsManager, MouseManager* mouseManager, const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile) : mBindingsManager(bindingsManager) , mMouseManager(mouseManager) , mGyroAvailable(false) , mGamepadGuiCursorEnabled(true) , mGuiCursorEnabled(true) , mJoystickLastUsed(false) { if (!controllerBindingsFile.empty()) { 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()) { 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 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)) { SDL_ControllerDeviceEvent evt; evt.which = i; static const int fakeDeviceID = 1; 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 { 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(); } } mBindingsManager->setJoystickDeadZone(Settings::input().mJoystickDeadZone); } void ControllerManager::update(float dt) { if (mGuiCursorEnabled && !(mJoystickLastUsed && !mGamepadGuiCursorEnabled)) { float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight) * 2.0f - 1.0f; float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward) * 2.0f - 1.0f; float zAxis = mBindingsManager->getActionValue(A_LookUpDown) * 2.0f - 1.0f; xAxis *= (1.5f - mBindingsManager->getActionValue(A_Use)); yAxis *= (1.5f - mBindingsManager->getActionValue(A_Use)); // 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(); 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) { mMouseManager->injectMouseMove(xMove, yMove, mouseWheelMove); mMouseManager->warpMouse(); MWBase::Environment::get().getWindowManager()->setCursorActive(true); } } 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 || yAxis != 0.5) { mJoystickLastUsed = true; MWBase::Environment::get().getInputManager()->resetIdleTime(); } } } void ControllerManager::buttonPressed(int deviceID, const SDL_ControllerButtonEvent& arg) { 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()) { if (gamepadToGuiControl(arg)) return; if (mGamepadGuiCursorEnabled) { // Temporary mouse binding until keyboard controls are available: if (arg.button == SDL_CONTROLLER_BUTTON_A) // We'll pretend that A is left click. { bool mousePressSuccess = mMouseManager->injectMouseButtonPress(SDL_BUTTON_LEFT); if (MyGUI::InputManager::getInstance().getMouseFocusWidget()) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled()) MWBase::Environment::get().getWindowManager()->playSound( ESM::RefId::stringRefId("Menu Click")); } mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); } } } else mBindingsManager->setPlayerControlsEnabled(true); // 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) { if (mBindingsManager->isDetectingBindingState()) { mBindingsManager->controllerButtonReleased(deviceID, arg); return; } 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; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { if (mGamepadGuiCursorEnabled) { // Temporary mouse binding until keyboard controls are available: 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. return; mBindingsManager->setPlayerControlsEnabled(!mousePressSuccess); } } } else mBindingsManager->setPlayerControlsEnabled(true); // 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) { if (!Settings::input().mEnableController || MWBase::Environment::get().getInputManager()->controlsDisabled()) return; mJoystickLastUsed = true; if (MWBase::Environment::get().getWindowManager()->isGuiMode()) { gamepadToGuiControl(arg); } else if (mBindingsManager->actionIsActive(A_TogglePOV) && (arg.axis == SDL_CONTROLLER_AXIS_TRIGGERRIGHT || arg.axis == SDL_CONTROLLER_AXIS_TRIGGERLEFT)) { // Preview Mode Gamepad Zooming; do not propagate to mBindingsManager return; } mBindingsManager->controllerAxisMoved(deviceID, arg); } void ControllerManager::controllerAdded(int deviceID, const SDL_ControllerDeviceEvent& arg) { mBindingsManager->controllerAdded(deviceID, arg); enableGyroSensor(); } void ControllerManager::controllerRemoved(const SDL_ControllerDeviceEvent& arg) { mBindingsManager->controllerRemoved(arg); } bool ControllerManager::gamepadToGuiControl(const SDL_ControllerButtonEvent& arg) { // Presumption of GUI mode will be removed in the future. // MyGUI KeyCodes *may* change. MyGUI::KeyCode key = MyGUI::KeyCode::None; switch (arg.button) { case SDL_CONTROLLER_BUTTON_DPAD_UP: key = MyGUI::KeyCode::ArrowUp; break; case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: key = MyGUI::KeyCode::ArrowRight; break; case SDL_CONTROLLER_BUTTON_DPAD_DOWN: key = MyGUI::KeyCode::ArrowDown; break; case SDL_CONTROLLER_BUTTON_DPAD_LEFT: key = MyGUI::KeyCode::ArrowLeft; break; case SDL_CONTROLLER_BUTTON_A: // If we are using the joystick as a GUI mouse, A must be handled via mouse. if (mGamepadGuiCursorEnabled) return false; key = MyGUI::KeyCode::Space; break; case SDL_CONTROLLER_BUTTON_B: if (MyGUI::InputManager::getInstance().isModalAny()) MWBase::Environment::get().getWindowManager()->exitCurrentModal(); else MWBase::Environment::get().getWindowManager()->exitCurrentGuiMode(); return true; case SDL_CONTROLLER_BUTTON_X: key = MyGUI::KeyCode::Semicolon; break; case SDL_CONTROLLER_BUTTON_Y: key = MyGUI::KeyCode::Apostrophe; break; case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::LeftShift); MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); MyGUI::InputManager::getInstance().injectKeyRelease(MyGUI::KeyCode::LeftShift); return true; case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Tab, 0, false); return true; case SDL_CONTROLLER_BUTTON_LEFTSTICK: mGamepadGuiCursorEnabled = !mGamepadGuiCursorEnabled; MWBase::Environment::get().getWindowManager()->setCursorActive(mGamepadGuiCursorEnabled); return true; default: return false; } // Some keys will work even when Text Input windows/modals are in focus. if (SDL_IsTextInputActive()) return false; MWBase::Environment::get().getWindowManager()->injectKeyPress(key, 0, false); return true; } bool ControllerManager::gamepadToGuiControl(const SDL_ControllerAxisEvent& arg) { switch (arg.axis) { case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: if (arg.value == 32767) // Treat like a button. MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Minus, 0, false); break; case SDL_CONTROLLER_AXIS_TRIGGERLEFT: if (arg.value == 32767) // Treat like a button. MWBase::Environment::get().getWindowManager()->injectKeyPress(MyGUI::KeyCode::Equals, 0, false); break; case SDL_CONTROLLER_AXIS_LEFTX: case SDL_CONTROLLER_AXIS_LEFTY: case SDL_CONTROLLER_AXIS_RIGHTX: case SDL_CONTROLLER_AXIS_RIGHTY: // If we are using the joystick as a GUI mouse, process mouse movement elsewhere. if (mGamepadGuiCursorEnabled) return false; break; default: return false; } 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 }); } } openmw-openmw-0.49.0/apps/openmw/mwinput/controllermanager.hpp000066400000000000000000000047031503074453300246260ustar00rootroot00000000000000#ifndef MWINPUT_MWCONTROLLERMANAGER_H #define MWINPUT_MWCONTROLLERMANAGER_H #include #include #include #include #include namespace MWInput { class BindingsManager; class MouseManager; class ControllerManager : public SDLUtil::ControllerListener { public: ControllerManager(BindingsManager* bindingsManager, MouseManager* mouseManager, const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile); virtual ~ControllerManager() = default; 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 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() const { return mJoystickLastUsed; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } void setGamepadGuiCursorEnabled(bool enabled) { mGamepadGuiCursorEnabled = enabled; } 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); void enableGyroSensor(); BindingsManager* mBindingsManager; MouseManager* mMouseManager; bool mGyroAvailable; bool mGamepadGuiCursorEnabled; bool mGuiCursorEnabled; bool mJoystickLastUsed; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/controlswitch.cpp000066400000000000000000000054051503074453300240050ustar00rootroot00000000000000#include "controlswitch.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWInput { ControlSwitch::ControlSwitch() { clear(); } void ControlSwitch::clear() { mSwitches["playercontrols"] = true; mSwitches["playerfighting"] = true; mSwitches["playerjumping"] = true; mSwitches["playerlooking"] = true; mSwitches["playermagic"] = true; mSwitches["playerviewswitch"] = true; mSwitches["vanitymode"] = true; } bool ControlSwitch::get(std::string_view 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(std::string_view key, bool value) { if (key == "playerlooking" && !value) { auto world = MWBase::Environment::get().getWorld(); world->rotateObject(world->getPlayerPtr(), osg::Vec3f()); } 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*/) { ESM::ControlsState controls; controls.mViewSwitchDisabled = !mSwitches["playerviewswitch"]; controls.mControlsDisabled = !mSwitches["playercontrols"]; controls.mJumpingDisabled = !mSwitches["playerjumping"]; controls.mLookingDisabled = !mSwitches["playerlooking"]; controls.mVanityModeDisabled = !mSwitches["vanitymode"]; controls.mWeaponDrawingDisabled = !mSwitches["playerfighting"]; controls.mSpellDrawingDisabled = !mSwitches["playermagic"]; writer.startRecord(ESM::REC_INPU); controls.save(writer); writer.endRecord(ESM::REC_INPU); } void ControlSwitch::readRecord(ESM::ESMReader& reader, uint32_t type) { ESM::ControlsState controls; controls.load(reader); set("playerviewswitch", !controls.mViewSwitchDisabled); set("playercontrols", !controls.mControlsDisabled); set("playerjumping", !controls.mJumpingDisabled); set("playerlooking", !controls.mLookingDisabled); set("vanitymode", !controls.mVanityModeDisabled); set("playerfighting", !controls.mWeaponDrawingDisabled); set("playermagic", !controls.mSpellDrawingDisabled); } int ControlSwitch::countSavedGameRecords() const { return 1; } } openmw-openmw-0.49.0/apps/openmw/mwinput/controlswitch.hpp000066400000000000000000000013501503074453300240050ustar00rootroot00000000000000#ifndef MWINPUT_CONTROLSWITCH_H #define MWINPUT_CONTROLSWITCH_H #include #include #include #include namespace ESM { struct ControlsState; class ESMReader; class ESMWriter; } namespace Loading { class Listener; } namespace MWInput { class ControlSwitch { public: ControlSwitch(); bool get(std::string_view key); void set(std::string_view key, bool value); void clear(); void write(ESM::ESMWriter& writer, Loading::Listener& progress); void readRecord(ESM::ESMReader& reader, uint32_t type); int countSavedGameRecords() const; private: std::map> mSwitches; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/gyromanager.cpp000066400000000000000000000047531503074453300234230ustar00rootroot00000000000000#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(); } } openmw-openmw-0.49.0/apps/openmw/mwinput/gyromanager.hpp000066400000000000000000000005701503074453300234210ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwinput/inputmanagerimp.cpp000066400000000000000000000174321503074453300243060ustar00rootroot00000000000000#include "inputmanagerimp.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" #include "actionmanager.hpp" #include "bindingsmanager.hpp" #include "controllermanager.hpp" #include "controlswitch.hpp" #include "gyromanager.hpp" #include "keyboardmanager.hpp" #include "mousemanager.hpp" #include "sensormanager.hpp" namespace MWInput { 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->setWindowEventCallback(MWBase::Environment::get().getWindowManager()); mInputWrapper->setKeyboardEventCallback(mKeyboardManager.get()); mInputWrapper->setMouseEventCallback(mMouseManager.get()); mInputWrapper->setControllerEventCallback(mControllerManager.get()); mInputWrapper->setSensorEventCallback(mSensorManager.get()); } void InputManager::clear() { // Enable all controls mControlSwitch->clear(); } InputManager::~InputManager() {} void InputManager::update(float dt, bool disableControls, bool disableEvents) { mControlsDisabled = disableControls; mInputWrapper->setMouseVisible(MWBase::Environment::get().getWindowManager()->getCursorVisible()); mInputWrapper->capture(disableEvents); if (disableControls) { mMouseManager->updateCursorMode(); return; } mBindingsManager->update(dt); mMouseManager->updateCursorMode(); mControllerManager->update(dt); mMouseManager->update(dt); mSensorManager->update(dt); mActionManager->update(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) { mBindingsManager->setDragDrop(dragDrop); } void InputManager::setGamepadGuiCursorEnabled(bool enabled) { mControllerManager->setGamepadGuiCursorEnabled(enabled); } bool InputManager::isGamepadGuiCursorEnabled() { return mControllerManager->gamepadGuiCursorEnabled(); } void InputManager::changeInputMode(bool guiMode) { mControllerManager->setGuiCursorEnabled(guiMode); mMouseManager->setGuiCursorEnabled(guiMode); mGyroManager->setGuiCursorEnabled(guiMode); mMouseManager->setMouseLookEnabled(!guiMode); if (guiMode) MWBase::Environment::get().getWindowManager()->showCrosshair(false); 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) { mSensorManager->processChangedSettings(changed); } bool InputManager::getControlSwitch(std::string_view sw) { return mControlSwitch->get(sw); } void InputManager::toggleControlSwitch(std::string_view sw, bool value) { mControlSwitch->set(sw, value); } void InputManager::resetIdleTime() { mActionManager->resetIdleTime(); } 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) const { return mBindingsManager->getActionKeyBindingName(action); } std::string InputManager::getActionControllerBindingName(int action) const { return mBindingsManager->getActionControllerBindingName(action); } bool InputManager::actionIsActive(int action) const { return mBindingsManager->actionIsActive(action); } float InputManager::getActionValue(int action) const { return mBindingsManager->getActionValue(action); } 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(); } const std::initializer_list& InputManager::getActionControllerSorting() { return mBindingsManager->getActionControllerSorting(); } void InputManager::enableDetectingBindingMode(int action, bool keyboard) { mBindingsManager->enableDetectingBindingMode(action, keyboard); } int InputManager::countSavedGameRecords() const { return mControlSwitch->countSavedGameRecords(); } void InputManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) { mControlSwitch->write(writer, progress); } void InputManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_INPU) { mControlSwitch->readRecord(reader, type); } } void InputManager::resetToDefaultKeyBindings() { mBindingsManager->loadKeyDefaults(true); } void InputManager::resetToDefaultControllerBindings() { mBindingsManager->loadControllerDefaults(true); } void InputManager::setJoystickLastUsed(bool enabled) { mControllerManager->setJoystickLastUsed(enabled); } bool InputManager::joystickLastUsed() { return mControllerManager->joystickLastUsed(); } void InputManager::executeAction(int action) { mActionManager->executeAction(action); } } openmw-openmw-0.49.0/apps/openmw/mwinput/inputmanagerimp.hpp000066400000000000000000000075771503074453300243240ustar00rootroot00000000000000#ifndef MWINPUT_MWINPUTMANAGERIMP_H #define MWINPUT_MWINPUTMANAGERIMP_H #include #include #include #include #include #include #include "../mwbase/inputmanager.hpp" #include "actions.hpp" namespace MWWorld { class Player; } namespace MWBase { class WindowManager; } namespace SDLUtil { class InputWrapper; } struct SDL_Window; namespace MWInput { class ControlSwitch; class ActionManager; class BindingsManager; class ControllerManager; class KeyboardManager; class MouseManager; class SensorManager; class GyroManager; /** * @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, const std::filesystem::path& userFile, bool userFileExists, const std::filesystem::path& userControllerBindingsFile, const std::filesystem::path& controllerBindingsFile, bool grab); ~InputManager() final; /// Clear all savegame-specific data void clear() override; void update(float dt, bool disableControls, bool disableEvents = false) override; void changeInputMode(bool guiMode) override; void processChangedSettings(const Settings::CategorySettingVector& changed) override; void setDragDrop(bool dragDrop) override; void setGamepadGuiCursorEnabled(bool enabled) override; bool isGamepadGuiCursorEnabled() 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; int getNumActions() override { return A_Last; } 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; void setJoystickLastUsed(bool enabled) override; bool joystickLastUsed() override; int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& progress) override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; void resetIdleTime() override; bool isIdle() const override; void executeAction(int action) override; bool controlsDisabled() override { return mControlsDisabled; } private: bool mControlsDisabled; 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 openmw-openmw-0.49.0/apps/openmw/mwinput/keyboardmanager.cpp000066400000000000000000000062561503074453300242430ustar00rootroot00000000000000#include "keyboardmanager.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "actions.hpp" #include "bindingsmanager.hpp" namespace MWInput { KeyboardManager::KeyboardManager(BindingsManager* bindingsManager) : mBindingsManager(bindingsManager) { } void KeyboardManager::textInput(const SDL_TextInputEvent& arg) { MyGUI::UString ustring(&arg.text[0]); MyGUI::UString::utf32string utf32string = ustring.asUTF32(); for (MyGUI::UString::utf32string::const_iterator it = utf32string.begin(); it != utf32string.end(); ++it) MyGUI::InputManager::getInstance().injectKeyPress(MyGUI::KeyCode::None, *it); } 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 = SDLUtil::sdlKeyToMyGUI(arg.keysym.sym); if (mBindingsManager->getKeyBinding(A_Console) == arg.keysym.scancode && MWBase::Environment::get().getWindowManager()->isConsoleMode()) SDL_StopTextInput(); bool consumed = SDL_IsTextInputActive() && // Little trick to check if key is printable (!(SDLK_SCANCODE_MASK & arg.keysym.sym) && // 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)) consumed = true; mBindingsManager->setPlayerControlsEnabled(!consumed); } if (arg.repeat) return; MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); 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) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); 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 }); } } openmw-openmw-0.49.0/apps/openmw/mwinput/keyboardmanager.hpp000066400000000000000000000011421503074453300242350ustar00rootroot00000000000000#ifndef MWINPUT_MWKEYBOARDMANAGER_H #define MWINPUT_MWKEYBOARDMANAGER_H #include namespace MWInput { class BindingsManager; class KeyboardManager : public SDLUtil::KeyListener { public: KeyboardManager(BindingsManager* bindingsManager); virtual ~KeyboardManager() = default; void textInput(const SDL_TextInputEvent& arg) override; void keyPressed(const SDL_KeyboardEvent& arg) override; void keyReleased(const SDL_KeyboardEvent& arg) override; private: BindingsManager* mBindingsManager; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/mousemanager.cpp000066400000000000000000000252731503074453300235730ustar00rootroot00000000000000#include "mousemanager.hpp" #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" namespace MWInput { 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; SDL_GetWindowSize(window, &w, &h); float uiScale = MWBase::Environment::get().getWindowManager()->getScalingFactor(); mGuiCursorX = w / (2.f * uiScale); mGuiCursorY = h / (2.f * uiScale); } void MouseManager::mouseMoved(const SDLUtil::MouseMotionEvent& arg) { mBindingsManager->mouseMoved(arg); MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); input->setJoystickLastUsed(false); input->resetIdleTime(); if (mGuiCursorEnabled) { input->setGamepadGuiCursorEnabled(true); // 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(); mGuiCursorX = static_cast(arg.x) / uiScale; mGuiCursorY = static_cast(arg.y) / uiScale; 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); MWBase::Environment::get().getWindowManager()->setCursorActive(true); } if (mMouseLookEnabled && !input->controlsDisabled()) { MWBase::World* world = MWBase::Environment::get().getWorld(); 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; rot[1] = 0.0f; rot[2] = -x; // Only actually turn player when we're not in vanity mode if (!world->vanityRotateCamera(rot) && input->getControlSwitch("playerlooking")) { MWWorld::Player& player = world->getPlayer(); player.yaw(x); player.pitch(y); } else if (!input->getControlSwitch("playerlooking")) MWBase::Environment::get().getWorld()->disableDeferredPreviewRotation(); } } void MouseManager::mouseReleased(const SDL_MouseButtonEvent& arg, Uint8 id) { MWBase::Environment::get().getInputManager()->setJoystickLastUsed(false); if (mBindingsManager->isDetectingBindingState()) { mBindingsManager->mouseReleased(arg, id); } else { bool guiMode = MWBase::Environment::get().getWindowManager()->isGuiMode(); 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 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) { 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) { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); input->setJoystickLastUsed(false); bool guiMode = false; 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), SDLUtil::sdlMouseButtonToMyGui(id)) && guiMode; if (MyGUI::InputManager::getInstance().getMouseFocusWidget() != nullptr) { MyGUI::Button* b = MyGUI::InputManager::getInstance().getMouseFocusWidget()->castType(false); if (b && b->getEnabled() && id == SDL_BUTTON_LEFT) { MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Menu Click")); } } MWBase::Environment::get().getWindowManager()->setCursorActive(true); } mBindingsManager->setPlayerControlsEnabled(!guiMode); // 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()->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(); bool wasRelative = mInputWrapper->getMouseRelative(); bool isRelative = !MWBase::Environment::get().getWindowManager()->isGuiMode(); // don't keep the pointer away from the window edge in gui mode // 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 && (Settings::input().mGrabCursor || isRelative)); // we switched to non-relative mode, move our cursor to where the in-game // cursor is if (!isRelative && wasRelative != isRelative) { warpMouse(); } } void MouseManager::update(float dt) { SDL_GetRelativeMouseState(&mMouseMoveX, &mMouseMoveY); if (!mMouseLookEnabled) return; float xAxis = mBindingsManager->getActionValue(A_LookLeftRight) * 2.0f - 1.0f; float yAxis = mBindingsManager->getActionValue(A_LookUpDown) * 2.0f - 1.0f; if (xAxis == 0 && yAxis == 0) return; 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 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(); } bool MouseManager::injectMouseButtonPress(Uint8 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), SDLUtil::sdlMouseButtonToMyGui(button)); } void MouseManager::injectMouseMove(float xMove, float yMove, float mouseWheelMove) { mGuiCursorX += xMove; mGuiCursorY += yMove; mMouseWheel += mouseWheelMove; const MyGUI::IntSize& viewSize = MyGUI::RenderManager::getInstance().getViewSize(); 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)); } void MouseManager::warpMouse() { float guiUiScale = Settings::gui().mScalingFactor; mInputWrapper->warpMouse( static_cast(mGuiCursorX * guiUiScale), static_cast(mGuiCursorY * guiUiScale)); } } openmw-openmw-0.49.0/apps/openmw/mwinput/mousemanager.hpp000066400000000000000000000031571503074453300235750ustar00rootroot00000000000000#ifndef MWINPUT_MWMOUSEMANAGER_H #define MWINPUT_MWMOUSEMANAGER_H #include #include namespace SDLUtil { class InputWrapper; } namespace MWInput { class BindingsManager; class MouseManager : public SDLUtil::MouseListener { public: MouseManager(BindingsManager* bindingsManager, SDLUtil::InputWrapper* inputWrapper, SDL_Window* window); virtual ~MouseManager() = default; 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; bool injectMouseButtonPress(Uint8 button); bool injectMouseButtonRelease(Uint8 button); void injectMouseMove(float xMove, float yMove, float mouseWheelMove); void warpMouse(); void setMouseLookEnabled(bool enabled) { mMouseLookEnabled = enabled; } void setGuiCursorEnabled(bool enabled) { mGuiCursorEnabled = enabled; } int getMouseMoveX() const { return mMouseMoveX; } int getMouseMoveY() const { return mMouseMoveY; } private: BindingsManager* mBindingsManager; SDLUtil::InputWrapper* mInputWrapper; float mGuiCursorX; float mGuiCursorY; int mMouseWheel; bool mMouseLookEnabled; bool mGuiCursorEnabled; int mMouseMoveX; int mMouseMoveY; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwinput/sensormanager.cpp000066400000000000000000000121111503074453300237370ustar00rootroot00000000000000#include "sensormanager.hpp" #include #include namespace MWInput { SensorManager::SensorManager() : mRotation() , mGyroValues() , mGyroUpdateTimer(0.f) , mGyroscope(nullptr) { init(); } void SensorManager::init() { correctGyroscopeAxes(); updateSensors(); } SensorManager::~SensorManager() { if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; } } void SensorManager::correctGyroscopeAxes() { 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. mRotation = osg::Matrixf::identity(); float angle = 0; SDL_DisplayOrientation currentOrientation = SDL_GetDisplayOrientation(Settings::video().mScreen); switch (currentOrientation) { case SDL_ORIENTATION_UNKNOWN: break; case SDL_ORIENTATION_LANDSCAPE: break; case SDL_ORIENTATION_LANDSCAPE_FLIPPED: { angle = osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT: { angle = -0.5 * osg::PIf; break; } case SDL_ORIENTATION_PORTRAIT_FLIPPED: { angle = 0.5 * osg::PIf; break; } } mRotation.makeRotate(angle, osg::Vec3f(0, 0, 1)); } void SensorManager::updateSensors() { if (Settings::input().mEnableGyroscope) { int numSensors = SDL_NumSensors(); for (int i = 0; i < numSensors; ++i) { 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. if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; mGyroUpdateTimer = 0.f; } // FIXME: SDL2 does not provide a way to configure a sensor update frequency so far. SDL_Sensor* sensor = SDL_SensorOpen(i); if (sensor == nullptr) Log(Debug::Error) << "Couldn't open sensor " << SDL_SensorGetDeviceName(i) << ": " << SDL_GetError(); else { mGyroscope = sensor; break; } } } } else { if (mGyroscope != nullptr) { SDL_SensorClose(mGyroscope); mGyroscope = nullptr; mGyroUpdateTimer = 0.f; } } } void SensorManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (const auto& setting : changed) { if (setting.first == "Input" && setting.second == "enable gyroscope") init(); } } void SensorManager::displayOrientationChanged() { correctGyroscopeAxes(); } void SensorManager::sensorUpdated(const SDL_SensorEvent& arg) { if (!Settings::input().mEnableGyroscope) return; SDL_Sensor* sensor = SDL_SensorFromInstanceID(arg.which); if (!sensor) { Log(Debug::Info) << "Couldn't get sensor for sensor event"; return; } switch (SDL_SensorGetType(sensor)) { case SDL_SENSOR_ACCEL: break; case SDL_SENSOR_GYRO: { osg::Vec3f gyro(arg.data[0], arg.data[1], arg.data[2]); mGyroValues = mRotation * gyro; mGyroUpdateTimer = 0.f; break; } default: break; } } void SensorManager::update(float dt) { 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. mGyroValues = osg::Vec3f(); mGyroUpdateTimer = 0.f; } } bool SensorManager::isGyroAvailable() const { return mGyroscope != nullptr; } std::array SensorManager::getGyroValues() const { return { mGyroValues.x(), mGyroValues.y(), mGyroValues.z() }; } } openmw-openmw-0.49.0/apps/openmw/mwinput/sensormanager.hpp000066400000000000000000000020441503074453300237500ustar00rootroot00000000000000#ifndef MWINPUT_MWSENSORMANAGER_H #define MWINPUT_MWSENSORMANAGER_H #include #include #include #include #include #include namespace SDLUtil { class InputWrapper; } namespace MWWorld { class Player; } namespace MWInput { class SensorManager : public SDLUtil::SensorListener { public: SensorManager(); virtual ~SensorManager(); void init(); void update(float dt); void sensorUpdated(const SDL_SensorEvent& arg) override; void displayOrientationChanged() override; void processChangedSettings(const Settings::CategorySettingVector& changed); bool isGyroAvailable() const; std::array getGyroValues() const; private: void updateSensors(); void correctGyroscopeAxes(); osg::Matrixf mRotation; osg::Vec3f mGyroValues; float mGyroUpdateTimer; SDL_Sensor* mGyroscope; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwlua/000077500000000000000000000000001503074453300200155ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwlua/README.md000066400000000000000000000125461503074453300213040ustar00rootroot00000000000000# 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. openmw-openmw-0.49.0/apps/openmw/mwlua/animationbindings.cpp000066400000000000000000000371511503074453300242250ustar00rootroot00000000000000#include "animationbindings.hpp" #include #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 { namespace { using BlendMask = MWRender::Animation::BlendMask; using BoneGroup = MWRender::Animation::BoneGroup; using Priority = MWMechanics::Priority; using AnimationPriorities = MWRender::Animation::AnimPriority; const MWWorld::Ptr& getMutablePtrOrThrow(const Object& object) { const MWWorld::Ptr& ptr = object.ptr(); if (!ptr.getRefData().isEnabled()) throw std::runtime_error("Can't use a disabled object"); return ptr; } MWRender::Animation* getMutableAnimationOrThrow(const Object& object) { const MWWorld::Ptr& ptr = getMutablePtrOrThrow(object); 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 Object& object) { auto world = MWBase::Environment::get().getWorld(); const MWRender::Animation* anim = world->getAnimation(object.ptr()); if (!anim) throw std::runtime_error("Object has no animation"); return anim; } 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) { using Misc::FiniteFloat; auto view = context.sol(); auto mechanics = MWBase::Environment::get().getMechanicsManager(); 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"] = [](const LObject& object) -> bool { return MWBase::Environment::get().getWorld()->getAnimation(object.ptr()) != nullptr; }; // equivalent to MWScript's SkipAnim api["skipAnimationThisFrame"] = [mechanics](const SelfObject& object) { const MWWorld::Ptr& ptr = getMutablePtrOrThrow(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 LObject& object, std::string_view key) -> sol::optional { float time = getConstAnimationOrThrow(object)->getTextKeyTime(key); if (time >= 0.f) return time; return sol::nullopt; }; api["isPlaying"] = [](const LObject& object, std::string_view groupname) { return getConstAnimationOrThrow(object)->isPlaying(groupname); }; api["getCurrentTime"] = [](const LObject& object, std::string_view groupname) -> sol::optional { float time = getConstAnimationOrThrow(object)->getCurrentTime(groupname); if (time >= 0.f) return time; return sol::nullopt; }; api["isLoopingAnimation"] = [](const LObject& object, std::string_view groupname) { return getConstAnimationOrThrow(object)->isLoopingAnimation(groupname); }; api["cancel"] = [](const SelfObject& object, std::string_view groupname) { return getMutableAnimationOrThrow(object)->disable(groupname); }; api["setLoopingEnabled"] = [](const SelfObject& object, std::string_view groupname, bool enabled) { return getMutableAnimationOrThrow(object)->setLoopingEnabled(groupname, enabled); }; // MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used. api["getCompletion"] = [](const LObject& object, std::string_view groupname) -> sol::optional { float completion = 0.f; if (getConstAnimationOrThrow(object)->getInfo(groupname, &completion)) return completion; return sol::nullopt; }; api["getLoopCount"] = [](const LObject& object, std::string groupname) -> sol::optional { size_t loops = 0; if (getConstAnimationOrThrow(object)->getInfo(groupname, nullptr, nullptr, &loops)) return loops; return sol::nullopt; }; api["getSpeed"] = [](const LObject& object, std::string groupname) -> sol::optional { float speed = 0.f; if (getConstAnimationOrThrow(object)->getInfo(groupname, nullptr, &speed, nullptr)) return speed; return sol::nullopt; }; api["setSpeed"] = [](const SelfObject& object, std::string groupname, const FiniteFloat speed) { getMutableAnimationOrThrow(object)->adjustSpeedMult(groupname, speed); }; api["getActiveGroup"] = [](const LObject& 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(object)->getActiveGroup(boneGroup); }; // Clears out the animation queue, and cancel any animation currently playing from the queue api["clearAnimationQueue"] = [mechanics](const SelfObject& object, bool clearScripted) { const MWWorld::Ptr& ptr = getMutablePtrOrThrow(object); mechanics->clearAnimationQueue(ptr, clearScripted); }; // Extended variant of MWScript's PlayGroup and LoopGroup api["playQueued"] = sol::overload( [mechanics](const SelfObject& 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); const MWWorld::Ptr& ptr = getMutablePtrOrThrow(object); mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop); }, [mechanics](const SelfObject& object, const std::string& groupname) { const MWWorld::Ptr& ptr = getMutablePtrOrThrow(object); mechanics->playAnimationGroupLua( ptr, groupname, std::numeric_limits::max(), 1, "start", "stop", false); }); api["playBlended"] = [](const SelfObject& 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(object); animation->play(lowerGroup, priority, blendMask, autoDisable, speed, start, stop, startPoint, loops, forceLoop || animation->isLoopingAnimation(lowerGroup)); }; api["hasGroup"] = [](const LObject& object, std::string_view groupname) -> bool { const MWRender::Animation* anim = getConstAnimationOrThrow(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 LObject& object, std::string_view bonename) -> bool { const MWRender::Animation* anim = getConstAnimationOrThrow(object); return anim->getNode(bonename) != nullptr; }; api["addVfx"] = [context](const SelfObject& object, std::string_view model, sol::optional options) { if (options) { context.mLuaManager->addAction( [object = Object(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", ""), useAmbientLight = options->get_or("useAmbientLight", true)] { MWRender::Animation* anim = getMutableAnimationOrThrow(object); anim->addEffect(model, effectId, loop, boneName, particleTexture, useAmbientLight); }, "addVfxAction"); } else { context.mLuaManager->addAction( [object = Object(object), model = std::string(model)] { MWRender::Animation* anim = getMutableAnimationOrThrow(object); anim->addEffect(model, ""); }, "addVfxAction"); } }; api["removeVfx"] = [context](const SelfObject& object, std::string_view effectId) { context.mLuaManager->addAction( [object = Object(object), effectId = std::string(effectId)] { MWRender::Animation* anim = getMutableAnimationOrThrow(object); anim->removeEffect(effectId); }, "removeVfxAction"); }; api["removeAllVfx"] = [context](const SelfObject& object) { context.mLuaManager->addAction( [object = Object(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); api["spawn"] = [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); bool useAmbientLight = options->get_or("useAmbientLight", true); context.mLuaManager->addAction( [model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, magicVfx, useAmbientLight]() { MWBase::Environment::get().getWorld()->spawnEffect( model, texture, worldPos, scale, magicVfx, useAmbientLight); }, "openmw.vfx.spawn"); } else { context.mLuaManager->addAction( [model = VFS::Path::Normalized(model), worldPos]() { MWBase::Environment::get().getWorld()->spawnEffect(model, "", worldPos, 1.f); }, "openmw.vfx.spawn"); } }; return api; } } openmw-openmw-0.49.0/apps/openmw/mwlua/animationbindings.hpp000066400000000000000000000004511503074453300242230ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/birthsignbindings.cpp000066400000000000000000000034271503074453300242360ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/birthsignbindings.hpp000066400000000000000000000003661503074453300242420ustar00rootroot00000000000000#ifndef MWLUA_BIRTHSIGNBINDINGS_H #define MWLUA_BIRTHSIGNBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initBirthSignRecordBindings(const Context& context); } #endif // MWLUA_BIRTHSIGNBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/camerabindings.cpp000066400000000000000000000155241503074453300234760ustar00rootroot00000000000000#include "camerabindings.hpp" #include #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) { using Misc::FiniteFloat; 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](const FiniteFloat v) { camera->setPitch(-v, true); if (camera->getMode() == CameraMode::ThirdPerson) camera->calculateDeferredRotation(); }; api["setYaw"] = [camera](const FiniteFloat v) { camera->setYaw(-v, true); if (camera->getMode() == CameraMode::ThirdPerson) camera->calculateDeferredRotation(); }; api["setRoll"] = [camera](const FiniteFloat v) { camera->setRoll(-v); }; api["setExtraPitch"] = [camera](const FiniteFloat v) { camera->setExtraPitch(-v); }; api["setExtraYaw"] = [camera](const FiniteFloat v) { camera->setExtraYaw(-v); }; api["setExtraRoll"] = [camera](const FiniteFloat 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](const FiniteFloat 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](const FiniteFloat 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](const FiniteFloat v) { renderingManager->setFieldOfView(osg::RadiansToDegrees(v)); }; api["getBaseViewDistance"] = [] { return Settings::camera().mViewingDistance.get(); }; api["getViewDistance"] = [renderingManager]() { return renderingManager->getViewDistance(); }; api["setViewDistance"] = [renderingManager](const FiniteFloat 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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/camerabindings.hpp000066400000000000000000000003101503074453300234660ustar00rootroot00000000000000#ifndef MWLUA_CAMERABINDINGS_H #define MWLUA_CAMERABINDINGS_H #include namespace MWLua { sol::table initCameraPackage(sol::state_view lua); } #endif // MWLUA_CAMERABINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/cellbindings.cpp000066400000000000000000000327411503074453300231650ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/cellbindings.hpp000066400000000000000000000003741503074453300231670ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/classbindings.cpp000066400000000000000000000043311503074453300233450ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/classbindings.hpp000066400000000000000000000003461503074453300233540ustar00rootroot00000000000000#ifndef MWLUA_CLASSBINDINGS_H #define MWLUA_CLASSBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initClassRecordBindings(const Context& context); } #endif // MWLUA_CLASSBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/context.hpp000066400000000000000000000056131503074453300222170ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/corebindings.cpp000066400000000000000000000156141503074453300231760ustar00rootroot00000000000000#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 readOnlyApi = LuaUtil::makeReadOnly(api); return context.setTypePackage(readOnlyApi, "openmw_core"); } } openmw-openmw-0.49.0/apps/openmw/mwlua/corebindings.hpp000066400000000000000000000004331503074453300231740ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/debugbindings.cpp000066400000000000000000000101341503074453300233240ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/debugbindings.hpp000066400000000000000000000003551503074453300233350ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/dialoguebindings.cpp000066400000000000000000000361131503074453300240340ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/dialoguebindings.hpp000066400000000000000000000003551503074453300240400ustar00rootroot00000000000000#ifndef MWLUA_DIALOGUEBINDINGS_H #define MWLUA_DIALOGUEBINDINGS_H #include namespace MWLua { struct Context; sol::table initCoreDialogueBindings(const Context& context); } #endif // MWLUA_DIALOGUEBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/engineevents.cpp000066400000000000000000000114061503074453300232150ustar00rootroot00000000000000#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(); } } openmw-openmw-0.49.0/apps/openmw/mwlua/engineevents.hpp000066400000000000000000000037771503074453300232360ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/factionbindings.cpp000066400000000000000000000107151503074453300236660ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/factionbindings.hpp000066400000000000000000000003541503074453300236710ustar00rootroot00000000000000#ifndef MWLUA_FACTIONBINDINGS_H #define MWLUA_FACTIONBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initCoreFactionBindings(const Context& context); } #endif // MWLUA_FACTIONBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/globalscripts.hpp000066400000000000000000000042321503074453300233770ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/idcollectionbindings.hpp000066400000000000000000000012201503074453300247070ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/inputbindings.cpp000066400000000000000000000575151503074453300234130ustar00rootroot00000000000000#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::object cached = context.getTypePackage("openmw_input"); if (cached != sol::nil) return cached; sol::state_view lua = context.sol(); context.cachePackage("openmw_input_keyevent", [&lua]() { 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; }); return sol::table(lua, sol::create); }); context.cachePackage("openmw_input_touchpadevent", [&lua]() { 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; }); return sol::table(lua, sol::create); }); context.cachePackage("openmw_input_inputactions", [&lua]() { 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; } return sol::table(lua, sol::create); }); context.cachePackage("openmw_input_actioninfo", [&lua]() { 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; }); return sol::table(lua, sol::create); }); context.cachePackage("openmw_input_inputtriggers", [&lua]() { 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; } return sol::table(lua, sol::create); }); context.cachePackage("openmw_input_triggerinfo", [&lua]() { 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; }); return sol::table(lua, sol::create); }); 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, persistent = context.mType == Context::Menu](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(); parsedOptions.mPersistent = persistent; 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, persistent = context.mType == Context::Menu](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(); parsedOptions.mPersistent = persistent; 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 }, })); sol::table readOnlyApi = LuaUtil::makeReadOnly(api); return context.setTypePackage(readOnlyApi, "openmw_input"); } } openmw-openmw-0.49.0/apps/openmw/mwlua/inputbindings.hpp000066400000000000000000000003271503074453300234050ustar00rootroot00000000000000#ifndef MWLUA_INPUTBINDINGS_H #define MWLUA_INPUTBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initInputPackage(const Context&); } #endif // MWLUA_INPUTBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/inputprocessor.hpp000066400000000000000000000102511503074453300236240ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/itemdata.cpp000066400000000000000000000172321503074453300223160ustar00rootroot00000000000000#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 }] = sol::main_object(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); }); } } } openmw-openmw-0.49.0/apps/openmw/mwlua/itemdata.hpp000066400000000000000000000003341503074453300223160ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/localscripts.cpp000066400000000000000000000307511503074453300232310ustar00rootroot00000000000000#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) -> bool { 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::ScriptTracker* tracker) : LuaUtil::ScriptsContainer(lua, "L" + obj.id().toString(), tracker, false) , 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(); } } openmw-openmw-0.49.0/apps/openmw/mwlua/localscripts.hpp000066400000000000000000000074671503074453300232460ustar00rootroot00000000000000#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, LuaUtil::ScriptTracker* tracker = nullptr); MWBase::LuaManager::ActorControls* getActorControls() { return &mData.mControls; } const MWWorld::Ptr& getPtrOrEmpty() const { return mData.ptrOrEmpty(); } void setActive(bool active); bool isActive() const override { return mData.mIsActive; } 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 openmw-openmw-0.49.0/apps/openmw/mwlua/luabindings.cpp000066400000000000000000000062551503074453300230300ustar00rootroot00000000000000#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.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.animation", initAnimationPackage(context) }, { "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) }, }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/luabindings.hpp000066400000000000000000000021011503074453300230170ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/luaevents.cpp000066400000000000000000000073401503074453300225330ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/luaevents.hpp000066400000000000000000000034471503074453300225440ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/luamanagerimp.cpp000066400000000000000000001042501503074453300233450ustar00rootroot00000000000000#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); } mLua.protectedCall([&](LuaUtil::LuaView& lua) { mScriptTracker.unloadInactiveScripts(lua); }); } 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() { mLua.protectedCall([&](LuaUtil::LuaView&) { synchronizedUpdateUnsafe(); }); } void LuaManager::synchronizedUpdateUnsafe() { 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(); mQueuedAutoStartedScripts.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; } 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)), &mScriptTracker); 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(true); mInputTriggers.clear(true); 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(); } } openmw-openmw-0.49.0/apps/openmw/mwlua/luamanagerimp.hpp000066400000000000000000000252661503074453300233630ustar00rootroot00000000000000#ifndef MWLUA_LUAMANAGERIMP_H #define MWLUA_LUAMANAGERIMP_H #include #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(); void synchronizedUpdateUnsafe(); 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; LuaUtil::ScriptTracker mScriptTracker; }; } #endif // MWLUA_LUAMANAGERIMP_H openmw-openmw-0.49.0/apps/openmw/mwlua/magicbindings.cpp000066400000000000000000001550611503074453300233270ustar00rootroot00000000000000#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(), spell = toSpell(spellOrId)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getSpells().add(spell, false); }); }; // 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(), spell = toSpell(spellOrId)]() { const MWWorld::Ptr& ptr = obj.ptr(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getSpells().remove(spell, false); }); }; // 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, context](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); context.mLuaManager->addAction([key, effects]() { // 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); }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/magicbindings.hpp000066400000000000000000000004571503074453300233320ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/markupbindings.cpp000066400000000000000000000016551503074453300235450ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/markupbindings.hpp000066400000000000000000000003331503074453300235420ustar00rootroot00000000000000#ifndef MWLUA_MARKUPBINDINGS_H #define MWLUA_MARKUPBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initMarkupPackage(const Context&); } #endif // MWLUA_MARKUPBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/menuscripts.cpp000066400000000000000000000130631503074453300231000ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/menuscripts.hpp000066400000000000000000000033451503074453300231070ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/mwscriptbindings.cpp000066400000000000000000000316661503074453300241230ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/mwscriptbindings.hpp000066400000000000000000000003461503074453300241170ustar00rootroot00000000000000#ifndef MWLUA_MWSCRIPTBINDINGS_H #define MWLUA_MWSCRIPTBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initMWScriptBindings(const Context&); } #endif // MWLUA_MWSCRIPTBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/nearbybindings.cpp000066400000000000000000000424441503074453300235270ustar00rootroot00000000000000#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 }, { "TargetPolygonNotFound", DetourNavigator::Status::TargetPolygonNotFound }, { "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"] = [lua](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 path; const DetourNavigator::Status status = DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); sol::table result(lua, sol::create); LuaUtil::copyVectorToTable(path, result); return std::make_tuple(status, 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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/nearbybindings.hpp000066400000000000000000000003331503074453300235230ustar00rootroot00000000000000#ifndef MWLUA_NEARBYBINDINGS_H #define MWLUA_NEARBYBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initNearbyPackage(const Context&); } #endif // MWLUA_NEARBYBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/object.hpp000066400000000000000000000035431503074453300220010ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/objectbindings.cpp000066400000000000000000001070131503074453300235070ustar00rootroot00000000000000#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); bool differentCell = ptr.getCell() != destCell; world->changeToCell(destCell->getCell()->getId(), toPos(pos, rot), false, differentCell); 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()); 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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/objectbindings.hpp000066400000000000000000000004061503074453300235120ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/objectlists.cpp000066400000000000000000000054171503074453300230550ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/openmw/mwlua/objectlists.hpp000066400000000000000000000034751503074453300230640ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/objectvariant.hpp000066400000000000000000000034571503074453300233720ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/playerscripts.hpp000066400000000000000000000037351503074453300234420ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/postprocessingbindings.cpp000066400000000000000000000136641503074453300253330ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/postprocessingbindings.hpp000066400000000000000000000003731503074453300253310ustar00rootroot00000000000000#ifndef MWLUA_POSTPROCESSINGBINDINGS_H #define MWLUA_POSTPROCESSINGBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initPostprocessingPackage(const Context&); } #endif // MWLUA_POSTPROCESSINGBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/racebindings.cpp000066400000000000000000000110461503074453300231530ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/racebindings.hpp000066400000000000000000000003421503074453300231550ustar00rootroot00000000000000#ifndef MWLUA_RACEBINDINGS_H #define MWLUA_RACEBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initRaceRecordBindings(const Context& context); } #endif // MWLUA_RACEBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/recordstore.hpp000066400000000000000000000050041503074453300230600ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/soundbindings.cpp000066400000000000000000000245101503074453300233710ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/soundbindings.hpp000066400000000000000000000004301503074453300233710ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/stats.cpp000066400000000000000000000730231503074453300216640ustar00rootroot00000000000000#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" }] = sol::main_object(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" }] = sol::main_object(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" }] = sol::main_object(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" }] = sol::main_object(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 }] = sol::main_object(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 }] = sol::main_object(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 }] = sol::main_object(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 }] = sol::main_object(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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/stats.hpp000066400000000000000000000005121503074453300216620ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/types/000077500000000000000000000000001503074453300211615ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwlua/types/activator.cpp000066400000000000000000000041371503074453300236660ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); record["mwscript"] = sol::readonly_property([](const ESM::Activator& rec) -> sol::optional { return LuaUtil::serializeRefId(rec.mScript); }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/actor.cpp000066400000000000000000000464671503074453300230160ustar00rootroot00000000000000#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); }; actor["getCapacity"] = [](const Object& actor) -> float { const MWWorld::Ptr ptr = actor.ptr(); return ptr.getClass().getCapacity(ptr); }; addActorStatsBindings(actor, context); addActorMagicBindings(actor, context); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/actor.hpp000066400000000000000000000073361503074453300230130ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/types/apparatus.cpp000066400000000000000000000047031503074453300236710ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/armor.cpp000066400000000000000000000117731503074453300230160ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/book.cpp000066400000000000000000000121541503074453300226220ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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); }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/clothing.cpp000066400000000000000000000112611503074453300234750ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/container.cpp000066400000000000000000000052551503074453300236560ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/creature.cpp000066400000000000000000000077671503074453300235200ustar00rootroot00000000000000#include "types.hpp" #include "../stats.hpp" #include "actor.hpp" #include "modelproperty.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; }); addModelProperty(record); 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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/door.cpp000066400000000000000000000154411503074453300226350ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); addModelProperty(record); record["isAutomatic"] = sol::readonly_property( [](const ESM4::Door& rec) -> bool { return rec.mDoorFlags & ESM4::Door::Flag_AutomaticDoor; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/ingredient.cpp000066400000000000000000000054311503074453300240200ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/item.cpp000066400000000000000000000021141503074453300226210ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/levelledlist.cpp000066400000000000000000000051521503074453300243600ustar00rootroot00000000000000#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([state](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(); }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/light.cpp000066400000000000000000000141721503074453300230010ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.hpp" #include #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) { sol::object color = rec["color"]; if (color.is()) light.mData.mColor = color.as().toRGBA(); else light.mData.mColor = color.as(); } 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; }); addModelProperty(record); 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) -> Misc::Color { return Misc::Color::fromRGB(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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/lockable.cpp000066400000000000000000000073251503074453300234500ustar00rootroot00000000000000#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); }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/lockpick.cpp000066400000000000000000000040501503074453300234630ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/misc.cpp000066400000000000000000000101331503074453300226160ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/modelproperty.hpp000066400000000000000000000011171503074453300245770ustar00rootroot00000000000000#ifndef OPENMW_APPS_OPENMW_MWLUA_TYPES_MODELPROPERTY_H #define OPENMW_APPS_OPENMW_MWLUA_TYPES_MODELPROPERTY_H #include #include #include #include namespace MWLua { template void addModelProperty(sol::usertype& recordType) { recordType["model"] = sol::readonly_property([](const T& recordValue) -> std::string { return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(recordValue.mModel)).value(); }); } } #endif openmw-openmw-0.49.0/apps/openmw/mwlua/types/npc.cpp000066400000000000000000000356621503074453300224610ustar00rootroot00000000000000#include "types.hpp" #include "actor.hpp" #include "modelproperty.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(); }); addModelProperty(record); 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; }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/player.cpp000066400000000000000000000301601503074453300231610ustar00rootroot00000000000000#include "types.hpp" #include #include #include "../birthsignbindings.hpp" #include "../luamanagerimp.hpp" #include "apps/openmw/mwbase/inputmanager.hpp" #include "apps/openmw/mwbase/journal.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 "apps/openmw/mwworld/globals.hpp" #include "apps/openmw/mwworld/player.hpp" 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; } ESM::RefId parseFactionId(std::string_view faction) { ESM::RefId id = ESM::RefId::deserializeText(faction); if (!MWBase::Environment::get().getESMStore()->get().search(id)) return ESM::RefId(); 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!"); } static void verifyNpc(const MWWorld::Class& cls) { if (!cls.isNpc()) throw std::runtime_error("The argument must be a NPC!"); } 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["OFFENSE_TYPE"] = LuaUtil::makeStrictReadOnly(LuaUtil::tableFromPairs(context.sol(), { { "Theft", MWBase::MechanicsManager::OffenseType::OT_Theft }, { "Assault", MWBase::MechanicsManager::OffenseType::OT_Assault }, { "Murder", MWBase::MechanicsManager::OffenseType::OT_Murder }, { "Trespassing", MWBase::MechanicsManager::OffenseType::OT_Trespassing }, { "SleepingInOwnedBed", MWBase::MechanicsManager::OffenseType::OT_SleepingInOwnedBed }, { "Pickpocket", MWBase::MechanicsManager::OffenseType::OT_Pickpocket } })); player["_runStandardCommitCrime"] = [](const Object& o, const sol::optional victim, int type, std::string_view faction, int arg = 0, bool victimAware = false) { verifyPlayer(o); if (victim.has_value() && !victim->ptrOrEmpty().isEmpty()) verifyNpc(victim->ptrOrEmpty().getClass()); if (!dynamic_cast(&o)) throw std::runtime_error("Only global scripts can commit crime"); if (type < 0 || type > MWBase::MechanicsManager::OffenseType::OT_Pickpocket) throw std::runtime_error("Invalid offense type"); ESM::RefId factionId = parseFactionId(faction); // If the faction is provided but not found, error out if (faction != "" && factionId == ESM::RefId()) throw std::runtime_error("Faction does not exist"); MWWorld::Ptr victimObj = nullptr; if (victim.has_value()) victimObj = victim->ptrOrEmpty(); return MWBase::Environment::get().getMechanicsManager()->commitCrime(o.ptr(), victimObj, static_cast(type), factionId, arg, victimAware); }; 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)); }; } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/potion.cpp000066400000000000000000000076241503074453300232060ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/probe.cpp000066400000000000000000000037231503074453300230010ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/repair.cpp000066400000000000000000000037451503074453300231600ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); 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; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/static.cpp000066400000000000000000000016431503074453300231600ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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(); }); addModelProperty(record); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/terminal.cpp000066400000000000000000000030371503074453300235030ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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; }); addModelProperty(record); } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/types.cpp000066400000000000000000000323551503074453300230410ustar00rootroot00000000000000#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"]; } } openmw-openmw-0.49.0/apps/openmw/mwlua/types/types.hpp000066400000000000000000000057551503074453300230520ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/types/weapon.cpp000066400000000000000000000174111503074453300231620ustar00rootroot00000000000000#include "types.hpp" #include "modelproperty.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::Last) 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; }); addModelProperty(record); 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]; }); } } openmw-openmw-0.49.0/apps/openmw/mwlua/uibindings.cpp000066400000000000000000000417031503074453300226610ustar00rootroot00000000000000#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 afterName, std::string_view name, const sol::object& opt) { LuaUi::Layer::Options options; options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); context.mLuaManager->addAction( [=]() { size_t index = LuaUi::Layer::indexOf(afterName); if (index == LuaUi::Layer::count()) throw std::logic_error( Misc::StringUtils::format("Couldn't insert after non-existent layer %s", afterName)); LuaUi::Layer::insert(index + 1, name, options); }, "Insert after UI layer"); }; layersTable["insertBefore"] = [context](std::string beforeName, std::string_view name, const sol::object& opt) { LuaUi::Layer::Options options; options.mInteractive = LuaUtil::getValueOrDefault(LuaUtil::getFieldOrNil(opt, "interactive"), true); context.mLuaManager->addAction( [=]() { size_t index = LuaUi::Layer::indexOf(beforeName); if (index == LuaUi::Layer::count()) throw std::logic_error( Misc::StringUtils::format("Couldn't insert before non-existent layer %s", beforeName)); LuaUi::Layer::insert(index, name, options); }, "Insert before 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 window, bool disabled) { luaManager->addAction( [=, window = std::move(window)]() { 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::main_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"); } } } openmw-openmw-0.49.0/apps/openmw/mwlua/uibindings.hpp000066400000000000000000000003261503074453300226620ustar00rootroot00000000000000#ifndef MWLUA_UIBINDINGS_H #define MWLUA_UIBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initUserInterfacePackage(const Context&); } #endif // MWLUA_UIBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/userdataserializer.cpp000066400000000000000000000106201503074453300244220ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/userdataserializer.hpp000066400000000000000000000013051503074453300244270ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/vfsbindings.cpp000066400000000000000000000317451503074453300230470ustar00rootroot00000000000000#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(lua_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(lua_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(lua_State* lua, Files::IStreamPtr& file) { double number = 0; if (file && *file >> number) return sol::make_object(lua, number); return sol::nil; } sol::object readCharactersFromFile(lua_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_main_state lua, sol::main_object self) { if (!self.is()) throw std::runtime_error("self should be a file handle"); return sol::as_function([lua, self]() -> sol::object { FileHandle* handle = self.as(); validateFile(*handle); return readLineFromFile(lua, *handle); }); }; api["lines"] = [vfs](sol::this_main_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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/vfsbindings.hpp000066400000000000000000000003171503074453300230430ustar00rootroot00000000000000#ifndef MWLUA_VFSBINDINGS_H #define MWLUA_VFSBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initVFSPackage(const Context&); } #endif // MWLUA_VFSBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwlua/worker.cpp000066400000000000000000000051311503074453300220320ustar00rootroot00000000000000#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(); } } } openmw-openmw-0.49.0/apps/openmw/mwlua/worker.hpp000066400000000000000000000022071503074453300220400ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwlua/worldbindings.cpp000066400000000000000000000244211503074453300233710ustar00rootroot00000000000000#include "worldbindings.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 "../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) { using Misc::FiniteFloat; MWWorld::DateTimeManager* timeManager = MWBase::Environment::get().getWorld()->getTimeManager(); api["setGameTimeScale"] = [timeManager](const FiniteFloat scale) { timeManager->setGameTimeScale(scale); }; api["setSimulationTimeScale"] = [context, timeManager](const FiniteFloat 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); } } openmw-openmw-0.49.0/apps/openmw/mwlua/worldbindings.hpp000066400000000000000000000003271503074453300233750ustar00rootroot00000000000000#ifndef MWLUA_WORLDBINDINGS_H #define MWLUA_WORLDBINDINGS_H #include #include "context.hpp" namespace MWLua { sol::table initWorldPackage(const Context&); } #endif // MWLUA_WORLDBINDINGS_H openmw-openmw-0.49.0/apps/openmw/mwmechanics/000077500000000000000000000000001503074453300211665ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwmechanics/activespells.cpp000066400000000000000000000641451503074453300244020ustar00rootroot00000000000000#include "activespells.hpp" #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 { bool merge(std::vector& present, const std::vector& queued) { // Can't merge if we already have an effect with the same effect index auto problem = std::find_if(queued.begin(), queued.end(), [&](const auto& qEffect) { return std::find_if(present.begin(), present.end(), [&](const auto& pEffect) { return pEffect.mEffectIndex == qEffect.mEffectIndex; }) != present.end(); }); if (problem != queued.end()) return false; present.insert(present.end(), queued.begin(), queued.end()); return true; } void addEffects( std::vector& effects, const ESM::EffectList& list, bool ignoreResistances = false) { for (const auto& enam : list.mList) { 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); } } } namespace MWMechanics { ActiveSpells::IterationGuard::IterationGuard(ActiveSpells& spells) : mActiveSpells(spells) { mActiveSpells.mIterating = true; } ActiveSpells::IterationGuard::~IterationGuard() { mActiveSpells.mIterating = false; } 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::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); } 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) { assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect); addEffects(mEffects, enchantment->mEffects); setFlag(ESM::ActiveSpells::Flag_Equipment); } 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 }) { } 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) { } ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const { 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; } void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag) { mFlags = static_cast(mFlags | flag); } void ActiveSpells::ActiveSpellParams::worsen() { ++mWorsenings; if (!mWorsenings) mNextWorsening = MWBase::Environment::get().getWorld()->getTimeStamp(); mNextWorsening += CorprusStats::sWorseningPeriod; } bool ActiveSpells::ActiveSpellParams::shouldWorsen() const { return mWorsenings >= 0 && MWBase::Environment::get().getWorld()->getTimeStamp() >= mNextWorsening; } void ActiveSpells::ActiveSpellParams::resetWorsenings() { mWorsenings = -1; } ESM::RefId ActiveSpells::ActiveSpellParams::getEnchantment() const { // Enchantment id is not stored directly. Instead the enchanted item is stored. const auto& store = MWBase::Environment::get().getESMStore(); switch (store->find(mSourceSpellId)) { 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 {}; } } const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const { return MWBase::Environment::get().getESMStore()->get().search(getSourceSpellId()); } bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const { return static_cast(mFlags & flags) == flags; } 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();) { 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; } for (const auto& spell : mQueue) addToSpells(ptr, spell); mQueue.clear(); // Vanilla only does this on cell change I think const auto& spells = creatureStats.getSpells(); for (const ESM::Spell* spell : spells) { if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power && !isSpellActive(spell->mId)) { mSpells.emplace_back(ActiveSpellParams{ spell, ptr }); mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId()); } } 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().search(enchantmentId); if (enchantment == nullptr || 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; } } } const MWWorld::Ptr player = MWMechanics::getPlayer(); bool updatedHitOverlay = false; bool updatedEnemy = false; // Update effects for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { const auto caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId( spellIt->mCasterActorId); // Maybe make this search outside active grid? bool removedSpell = false; std::optional reflected; for (auto it = spellIt->mEffects.begin(); it != spellIt->mEffects.end();) { auto result = applyMagicEffect(ptr, caster, *spellIt, *it, duration); if (result.mType == MagicApplicationResult::Type::REFLECTED) { if (!reflected) { if (Settings::game().mClassicReflectedAbsorbSpellsBehavior) reflected = { *spellIt, caster }; else reflected = { *spellIt, ptr }; } auto& reflectedEffect = reflected->mEffects.emplace_back(*it); reflectedEffect.mFlags = ESM::ActiveEffect::Flag_Ignore_Reflect | ESM::ActiveEffect::Flag_Ignore_SpellAbsorption; it = spellIt->mEffects.erase(it); } else if (result.mType == MagicApplicationResult::Type::REMOVED) it = spellIt->mEffects.erase(it); else { ++it; if (!updatedEnemy && result.mShowHealth && caster == player && ptr != player) { MWBase::Environment::get().getWindowManager()->setEnemy(ptr); updatedEnemy = true; } if (!updatedHitOverlay && result.mShowHit && ptr == player) { MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); updatedHitOverlay = true; } } removedSpell = applyPurges(ptr, &spellIt, &it); if (removedSpell) break; } if (reflected) { const ESM::Static* reflectStatic = MWBase::Environment::get().getESMStore()->get().find( ESM::RefId::stringRefId("VFX_Reflect")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation && !reflectStatic->mModel.empty()) { const VFS::Path::Normalized reflectStaticModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(reflectStatic->mModel)); animation->addEffect( reflectStaticModel, ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); } caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } if (removedSpell) continue; 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(); } } else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment)) { // 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::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell) { if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable)) { 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()) { 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()); } ActiveSpells::ActiveSpells() : mIterating(false) { } ActiveSpells::TIterator ActiveSpells::begin() const { return mSpells.begin(); } ActiveSpells::TIterator ActiveSpells::end() const { return mSpells.end(); } ActiveSpells::TIterator ActiveSpells::getActiveSpellById(const ESM::RefId& id) { for (TIterator it = begin(); it != end(); it++) if (it->getActiveSpellId() == id) return it; return end(); } 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::addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor) { 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) { IterationGuard guard{ *this }; applyPurges(ptr); } } void ActiveSpells::purge(EffectPredicate predicate, const MWWorld::Ptr& ptr) { assert(&ptr.getClass().getCreatureStats(ptr).getActiveSpells() == this); mPurges.emplace(predicate); if (!mIterating) { IterationGuard guard{ *this }; applyPurges(ptr); } } bool ActiveSpells::applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell, std::vector::iterator* currentEffect) { bool removedCurrentSpell = false; while (!mPurges.empty()) { auto predicate = mPurges.front(); mPurges.pop(); for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();) { 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); } } return removedCurrentSpell; } void ActiveSpells::removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) { purge([=](const ActiveSpellParams& params) { return params.mSourceSpellId == id; }, ptr); } void ActiveSpells::removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id) { purge([=](const ActiveSpellParams& params) { return params.mActiveSpellId == id; }, ptr); } void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg) { purge( [=](const ActiveSpellParams&, const ESM::ActiveEffect& effect) { if (effectArg.empty()) return effect.mEffectId == effectId; return effect.mEffectId == effectId && effect.getSkillOrAttribute() == effectArg; }, ptr); } void ActiveSpells::purge(const MWWorld::Ptr& ptr, int casterActorId) { 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); } void ActiveSpells::skipWorsenings(double hours) { for (auto& spell : mSpells) { if (spell.mWorsenings >= 0) spell.mNextWorsening += hours; } } void ActiveSpells::writeState(ESM::ActiveSpells& state) const { 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) { 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(); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/activespells.hpp000066400000000000000000000132001503074453300243710ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_ACTIVESPELLS_H #define GAME_MWMECHANICS_ACTIVESPELLS_H #include #include #include #include #include #include #include #include "../mwworld/ptr.hpp" #include "../mwworld/timestamp.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 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); ESM::RefId getActiveSpellId() const { return mActiveSpellId; } void setActiveSpellId(ESM::RefId id) { mActiveSpellId = id; } const ESM::RefId& getSourceSpellId() const { return mSourceSpellId; } const std::vector& getEffects() const { return mEffects; } std::vector& getEffects() { return mEffects; } int getCasterActorId() const { return mCasterActorId; } int getWorsenings() const { return mWorsenings; } const std::string& getDisplayName() const { return mDisplayName; } ESM::RefNum getItem() const { return mItem; } ESM::RefId getEnchantment() const; const ESM::Spell* getSpell() const; bool hasFlag(ESM::ActiveSpells::Flags flags) const; void setFlag(ESM::ActiveSpells::Flags flags); // Increments worsenings count and sets the next timestamp void worsen(); bool shouldWorsen() const; void resetWorsenings(); }; typedef std::list Collection; typedef Collection::const_iterator TIterator; void readState(const ESM::ActiveSpells& state); void writeState(ESM::ActiveSpells& state) const; TIterator begin() const; 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); bool applyPurges(const MWWorld::Ptr& ptr, std::list::iterator* currentSpell = nullptr, std::vector::iterator* currentEffect = nullptr); public: ActiveSpells(); /// Add lasting effects /// /// \brief addSpell /// \param id ID for stacking purposes. /// void addSpell(const ActiveSpellParams& params); /// Bypasses resistances void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor); /// Removes the active effects from this spell/potion/.. with \a id void 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 with this effect id void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {}); void purge(EffectPredicate predicate, const MWWorld::Ptr& ptr); void purge(ParamsPredicate predicate, const MWWorld::Ptr& ptr); /// Remove all effects that were cast by \a casterActorId void purge(const MWWorld::Ptr& ptr, int casterActorId); /// Remove all spells void clear(const MWWorld::Ptr& ptr); /// 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; /// True if the enchantment is active bool isEnchantmentActive(const ESM::RefId& id) const; void skipWorsenings(double hours); void unloadActor(const MWWorld::Ptr& ptr); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/actor.hpp000066400000000000000000000050731503074453300230140ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_ACTOR_H #define OPENMW_MECHANICS_ACTOR_H #include #include "character.hpp" #include "creaturestats.hpp" #include "greetingstate.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include namespace MWRender { class Animation; } namespace MWWorld { class Ptr; } namespace MWMechanics { /// @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) : 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) { mCharacterController.updatePtr(newPtr); } CharacterController& getCharacterController() { return mCharacterController; } const CharacterController& getCharacterController() const { return mCharacterController; } int getGreetingTimer() const { return mGreetingTimer; } void setGreetingTimer(int timer) { mGreetingTimer = timer; } float getAngleToPlayer() const { return mTargetAngleRadians; } void setAngleToPlayer(float angle) { mTargetAngleRadians = angle; } GreetingState getGreetingState() const { return mGreetingState; } void setGreetingState(GreetingState state) { mGreetingState = state; } bool isTurningToPlayer() const { return mIsTurningToPlayer; } void setTurningToPlayer(bool turning) { mIsTurningToPlayer = turning; } Misc::TimerStatus updateEngageCombatTimer(float duration) { return mEngageCombat.update(duration, MWBase::Environment::get().getWorld()->getPrng()); } void setPositionAdjusted(bool adjusted) { mPositionAdjusted = adjusted; } bool getPositionAdjusted() const { return mPositionAdjusted; } private: CharacterController mCharacterController; int mGreetingTimer{ 0 }; float mTargetAngleRadians{ 0.f }; GreetingState mGreetingState{ Greet_None }; bool mIsTurningToPlayer{ false }; Misc::DeviatingPeriodicTimer mEngageCombat{ 1.0f, 0.25f, Misc::Rng::deviate(0, 0.25f, MWBase::Environment::get().getWorld()->getPrng()) }; bool mPositionAdjusted; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/actors.cpp000066400000000000000000003134431503074453300231750ustar00rootroot00000000000000#include "actors.hpp" #include #include #include #include #include #include #include #include #include #include #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/player.hpp" #include "../mwworld/scene.hpp" #include "../mwbase/dialoguemanager.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 "../mwsound/constants.hpp" #include "actor.hpp" #include "actorutil.hpp" #include "aicombataction.hpp" #include "aifollow.hpp" #include "aipursue.hpp" #include "aiwander.hpp" #include "attacktype.hpp" #include "character.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "steering.hpp" #include "summoning.hpp" namespace { bool isConscious(const MWWorld::Ptr& ptr) { const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); return !stats.isDead() && !stats.getKnockedDown(); } 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; } // Check for command effects having ended and remove package if necessary void adjustCommandedActor(const MWWorld::Ptr& actor) { if (isCommanded(actor)) return; MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); stats.getAiSequence().erasePackageIf([](auto& entry) { if (entry->getTypeId() == MWMechanics::AiPackageTypeId::Follow && static_cast(entry.get())->isCommanded()) { return true; } return false; }); } std::pair getRestorationPerHourOfSleep(const MWWorld::Ptr& ptr) { const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); const MWWorld::Store& settings = MWBase::Environment::get().getESMStore()->get(); const float endurance = stats.getAttribute(ESM::Attribute::Endurance).getModified(); const float health = 0.1f * endurance; static const float fRestMagicMult = settings.find("fRestMagicMult")->mValue.getFloat(); const float magicka = fRestMagicMult * stats.getAttribute(ESM::Attribute::Intelligence).getModified(); return { health, magicka }; } template void forEachFollowingPackage( const std::list& actors, const MWWorld::Ptr& actorPtr, const MWWorld::Ptr& player, T&& func) { for (const MWMechanics::Actor& actor : actors) { const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == player || iteratedActor == actorPtr) continue; const MWMechanics::CreatureStats& stats = iteratedActor.getClass().getCreatureStats(iteratedActor); if (stats.isDead()) continue; // 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; } } } float getStuntedMagickaDuration(const MWWorld::Ptr& actor) { float remainingTime = 0.f; for (const auto& params : actor.getClass().getCreatureStats(actor).getActiveSpells()) { 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; } 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; // 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; // Set the soul on just one of the gems, not the whole stack gem->getContainerStore()->unstack(*gem); gem->getCellRef().setSoul(creature.getCellRef().getRefId()); // Restack the gem with other gems with the same soul gem->getContainerStore()->restack(*gem); if (caster == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sSoultrapSuccess}"); const ESM::Static* const fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Soul_Trap")); if (fx != nullptr) world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)), "", creature.getRefData().getPosition().asVec3()); 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 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 { std::string_view attackTypeName(AttackType attackType) { switch (attackType) { 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))); } 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 updateHeadTracking(const MWWorld::Ptr& actor, const MWWorld::Ptr& targetActor, MWWorld::Ptr& headTrackTarget, float& sqrHeadTrackDistance, bool inCombatOrPursue) { const auto& actorRefData = actor.getRefData(); if (!actorRefData.getBaseNode()) return; if (targetActor.getClass().getCreatureStats(targetActor).isDead()) return; if (isTargetMagicallyHidden(targetActor)) return; 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; // 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 updateHeadTracking( const MWWorld::Ptr& ptr, const std::list& actors, bool isPlayer, CharacterController& ctrl) { float sqrHeadTrackDistance = std::numeric_limits::max(); MWWorld::Ptr headTrackTarget; const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); const bool firstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); // 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) { bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().isInPursuit(); if (inCombatOrPursue) { auto activePackageTarget = stats.getAiSequence().getActivePackage().getTarget(); if (!activePackageTarget.isEmpty()) { // Track the specified target of package. updateHeadTracking( ptr, activePackageTarget, headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } else { // Find something nearby. for (const Actor& otherActor : actors) { if (otherActor.getPtr() == ptr) continue; updateHeadTracking( ptr, otherActor.getPtr(), headTrackTarget, sqrHeadTrackDistance, inCombatOrPursue); } } } ctrl.setHeadTrackTarget(headTrackTarget); } void updateLuaControls(const MWWorld::Ptr& ptr, bool isPlayer, MWBase::LuaManager::ActorControls& controls) { 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) { 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; } } void Actors::updateActor(const MWWorld::Ptr& ptr, float duration) const { // magic effects adjustMagicEffects(ptr, duration); // fatigue restoration calculateRestoration(ptr, duration); } void Actors::playIdleDialogue(const MWWorld::Ptr& actor) const { if (!actor.getClass().isActor() || actor == getPlayer() || MWBase::Environment::get().getSoundManager()->sayActive(actor)) return; const CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getAiSetting(AiSetting::Hello).getModified() == 0) return; const MWMechanics::AiSequence& seq = stats.getAiSequence(); if (seq.isInCombat() || seq.hasPackage(AiPackageTypeId::Follow) || seq.hasPackage(AiPackageTypeId::Escort)) return; const osg::Vec3f playerPos(getPlayer().getRefData().getPosition().asVec3()); const osg::Vec3f actorPos(actor.getRefData().getPosition().asVec3()); 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(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) const { if (Settings::game().mSmoothMovement) return; const auto& actorClass = actor.getClass(); const CreatureStats& stats = actorClass.getCreatureStats(actor); const MWMechanics::AiSequence& seq = stats.getAiSequence(); if (!seq.isEmpty() && seq.getActivePackage().useVariableSpeed()) { 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) { 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; } } } void Actors::updateGreetingState(const MWWorld::Ptr& actor, Actor& actorState, bool turnOnly) { const auto& actorClass = actor.getClass(); if (!actorClass.isActor() || actor == getPlayer()) return; 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)) { actorState.setTurningToPlayer(false); actorState.setGreetingTimer(0); actorState.setGreetingState(Greet_None); return; } 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()) { // Reduce the turning animation glitch by using a *HUGE* value of // epsilon... TODO: a proper fix might be in either the physics or the // animation subsystem if (zTurn(actor, actorState.getAngleToPlayer(), osg::DegreesToRadians(5.f))) { actorState.setTurningToPlayer(false); // An original engine launches an endless idle2 when an actor greets player. playAnimationGroup(actor, "idle2", 0, std::numeric_limits::max(), false); } } if (turnOnly) return; // Play a random voice greeting if the player gets too close static const int iGreetDistanceMultiplier = MWBase::Environment::get() .getESMStore() ->get() .find("iGreetDistanceMultiplier") ->mValue.getInteger(); 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 && !playerStats.isDead() && !actorStats.isParalyzed() && !isTargetMagicallyHidden(player) && MWBase::Environment::get().getWorld()->getLOS(player, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor)) greetingTimer++; if (greetingTimer >= GREETING_SHOULD_START) { greetingState = Greet_InProgress; if (!MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("hello"))) greetingState = Greet_Done; greetingTimer = 0; } } if (greetingState == Greet_InProgress) { greetingTimer++; 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) { greetingState = Greet_Done; greetingTimer = 0; } } if (greetingState == Greet_Done) { float resetDist = 2 * helloDistance; if ((playerPos - actorPos).length2() >= resetDist * resetDist) greetingState = Greet_None; } actorState.setGreetingTimer(greetingTimer); actorState.setGreetingState(greetingState); } void Actors::turnActorToFacePlayer(const MWWorld::Ptr& actor, Actor& actorState, const osg::Vec3f& dir) const { auto& movementSettings = actor.getClass().getMovementSettings(actor); movementSettings.mPosition[1] = 0; movementSettings.mPosition[0] = 0; if (!actorState.isTurningToPlayer()) { float from = dir.x(); float to = dir.y(); float angle = std::atan2(from, to); actorState.setAngleToPlayer(angle); float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]); if (!Settings::game().mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) actorState.setTurningToPlayer(true); } } 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)) return; CreatureStats& creatureStats1 = actor1.getClass().getCreatureStats(actor1); if (creatureStats1.isDead() || creatureStats1.getAiSequence().isInCombat(actor2)) return; const CreatureStats& creatureStats2 = actor2.getClass().getCreatureStats(actor2); if (creatureStats2.isDead()) return; const osg::Vec3f actor1Pos(actor1.getRefData().getPosition().asVec3()); const osg::Vec3f actor2Pos(actor2.getRefData().getPosition().asVec3()); const float sqrDist = (actor1Pos - actor2Pos).length2(); const int actorsProcessingRange = Settings::game().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 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 const std::set& allies1 = cachedAllies.getActorsSidingWith(actor1); 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())) { 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 (ally.getClass().getCreatureStats(ally).getAiSequence().isInCombat(actor2)) aggressive = true; } MWWorld::Ptr player = MWMechanics::getPlayer(); const std::set& playerAllies = cachedAllies.getActorsSidingWith(player); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); // If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them // Doesn't apply for player followers/escorters if (!aggressive && !isPlayerFollowerOrEscorter) { // Check that actor2 is in combat with actor1 if (creatureStats2.getAiSequence().isInCombat(actor1)) { 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) { if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) { mechanicsManager->startCombat(actor1, actor2, &allies2); // Also have actor1's allies start combat for (const MWWorld::Ptr& ally1 : allies1) 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 if (!aggressive && isPlayerFollowerOrEscorter && Settings::game().mFollowersAttackOnSight) { if (creatureStats2.getAiSequence().isInCombat(actor1)) aggressive = true; else { for (const MWWorld::Ptr& ally : allies1) { if (ally != actor1 && creatureStats2.getAiSequence().isInCombat(ally)) { aggressive = true; break; } } } } // Do aggression check if actor2 is the player or a player follower or escorter if (!aggressive) { if (againstPlayer || playerAllies.find(actor2) != playerAllies.end()) { // Player followers and escorters with high fight should not initiate combat with the player or with // other player followers or escorters if (!isPlayerFollowerOrEscorter) aggressive = mechanicsManager->isAggressive(actor1, actor2); } } // 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 = world->getStore().get().find("fAlarmRadius")->mValue.getFloat(); if (sqrDist > fAlarmRadius * fAlarmRadius) return; bool targetIsCreature = !actor2.getClass().isNpc(); if (targetIsCreature || actor2.getClass().getNpcStats(actor2).isWerewolf()) { bool followerOrEscorter = false; // ...unless the creature has allies if (targetIsCreature) { 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; } } // 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; } } // 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 = 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(); const MWWorld::Ptr player = getPlayer(); std::set playerFollowers; getActorsSidingWith(player, playerFollowers); for (const ActiveSpells::ActiveSpellParams& spell : spells) { bool actorKilled = false; MWWorld::Ptr caster = MWBase::Environment::get().getWorld()->searchPtrViaActorId(spell.getCasterActorId()); if (caster.isEmpty()) continue; for (const auto& effect : spell.getEffects()) { static const std::array damageEffects{ ESM::MagicEffect::FireDamage, ESM::MagicEffect::ShockDamage, ESM::MagicEffect::FrostDamage, ESM::MagicEffect::Poison, ESM::MagicEffect::SunDamage, ESM::MagicEffect::DamageHealth, ESM::MagicEffect::AbsorbHealth, }; const bool isDamageEffect = std::find(damageEffects.begin(), damageEffects.end(), effect.mEffectId) != damageEffects.end(); if (isDamageEffect) { if (caster.getClass().isNpc() && caster.getClass().getNpcStats(caster).isWerewolf()) caster.getClass().getNpcStats(caster).addWerewolfKill(); if (caster == player || playerFollowers.find(caster) != playerFollowers.end()) { MWBase::Environment::get().getMechanicsManager()->actorKilled(creature, player); actorKilled = true; break; } } } if (actorKilled) break; } } // updateSummons assumes the actor belongs to a cell. // This assumption isn't always valid for the player character. if (!creature.isInCell()) return; if (!creatureStats.getSummonedCreatureMap().empty() || !creatureStats.getSummonedCreatureGraveyard().empty()) updateSummons(creature, mTimerDisposeSummonsCorpses == 0.f); } 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 (sleep) { const auto [health, magicka] = getRestorationPerHourOfSleep(ptr); DynamicStat stat = stats.getHealth(); stat.setCurrent(stat.getCurrent() + health * hours); stats.setHealth(stat); 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); // 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; } 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 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::calculateRestoration(const MWWorld::Ptr& ptr, float duration) const { 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 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) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isAttackPreparing(); } bool Actors::isRunning(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isRunning(); } bool Actors::isSneaking(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isSneaking(); } static void updateDrowning(const MWWorld::Ptr& ptr, float duration, bool isKnockedOut, bool isPlayer) { 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() .getESMStore() ->get() .find("fHoldBreathTime") ->mValue.getFloat(); if (stats.getTimeToStartDrowning() == -1.f) stats.setTimeToStartDrowning(fHoldBreathTime); if (!isPlayer && stats.getTimeToStartDrowning() < fHoldBreathTime / 2) { AiSequence& seq = actorClass.getCreatureStats(ptr).getAiSequence(); if (seq.getTypeId() != AiPackageTypeId::Breathe) // Only add it once seq.stack(AiBreathe(), ptr); } 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) stats.setTimeToStartDrowning(0); else { timeLeft = stats.getTimeToStartDrowning() - duration; if (timeLeft < 0.0f) timeLeft = 0.0f; stats.setTimeToStartDrowning(timeLeft); } const bool godmode = isPlayer && world->getGodModeState(); 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(); DynamicStat health = stats.getHealth(); health.setCurrent(health.getCurrent() - fSuffocationDamage * duration); stats.setHealth(health); // Play a drowning sound 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) MWBase::Environment::get().getWindowManager()->activateHitOverlay(false); } } else stats.setTimeToStartDrowning(fHoldBreathTime); } static void updateEquippedLight(const MWWorld::Ptr& ptr, float duration, bool mayEquip) { const bool isPlayer = (ptr == getPlayer()); const auto& actorClass = ptr.getClass(); auto& inventoryStore = actorClass.getInventoryStore(ptr); auto heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); /** * Automatically equip NPCs torches at night and unequip them at day */ if (!isPlayer) { 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 (torchIter != inventoryStore.end()) { 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->getType() != ESM::Light::sRecordId) inventoryStore.unequipItem(*heldIter); } 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(); if (shield != inventoryStore.end()) inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, shield); } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // If we have a torch and can equip it, then equip it now. if (heldIter == inventoryStore.end()) { inventoryStore.equip(MWWorld::InventoryStore::Slot_CarriedLeft, torchIter); } } } else { 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(); } } } heldIter = inventoryStore.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); // 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) { float timeRemaining = heldIter->getClass().getRemainingUsageTime(*heldIter); // -1 is infinite light source. Other negative values are treated as 0. if (timeRemaining != -1.0f) { timeRemaining -= duration; if (timeRemaining <= 0.f) { inventoryStore.remove(*heldIter, 1); // remove it return; } heldIter->getClass().setRemainingUsageTime(*heldIter, timeRemaining); } } // Both NPC and player lights extinguish in water. if (world->isSwimming(ptr)) { inventoryStore.remove(*heldIter, 1); // remove it // ...But, only the player makes a sound. 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, SidingCache& cachedAllies) const { const MWWorld::Ptr player = getPlayer(); if (ptr == player) return; const auto& actorClass = ptr.getClass(); if (!actorClass.isNpc()) return; // 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; 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)) { static const int iCrimeThresholdMultiplier = esmStore.get().find("iCrimeThresholdMultiplier")->mValue.getInteger(); if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier) { 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()); } } // 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); // Reset factors to attack creatureStats.setAttacked(false); creatureStats.setAlarmed(false); creatureStats.setAiSetting(AiSetting::Fight, ptr.getClass().getBaseFightRating(ptr)); // Restore original disposition npcStats.setCrimeDispositionModifier(0); // Update witness crime id npcStats.setCrimeId(-1); } } } void Actors::addActor(const MWWorld::Ptr& ptr, bool updateImmediately) { removeActor(ptr, true); MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (!anim) return; const auto it = mActors.emplace(mActors.end(), ptr, anim); mIndex.emplace(ptr.mRef, it); if (updateImmediately) 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. // Do not update visibility if player was teleported, so actors will be visible during teleportation frame. if (MWBase::Environment::get().getWorld()->getPlayer().wasTeleported()) return; updateVisibility(ptr, it->getCharacterController()); } 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(); const int actorsProcessingRange = Settings::game().mActorsProcessingRange; if (dist > actorsProcessingRange) { ptr.getRefData().getBaseNode()->setNodeMask(0); return; } else ptr.getRefData().getBaseNode()->setNodeMask(MWRender::Mask_Actor); // Fade away actors on large distance (>90% of actor's processing distance) float visibilityRatio = 1.0; 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); } void Actors::removeActor(const MWWorld::Ptr& ptr, bool keepActive) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { if (!keepActive) removeTemporaryEffects(iter->second->getPtr()); mActors.erase(iter->second); mIndex.erase(iter); } } void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) const { 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) const { if (!actor.getClass().isActor()) return false; // 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); } // 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, Settings::game().mActorsProcessingRange, neighbors); for (const MWWorld::Ptr& neighbor : neighbors) { if (neighbor == actor) continue; const bool result = MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor, neighbor); if (result) return true; } return false; } void Actors::updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const { 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) { for (auto iter = mActors.begin(); iter != mActors.end();) { if ((iter->getPtr().isInCell() && iter->getPtr().getCell() == cellStore) && iter->getPtr() != ignore) { removeTemporaryEffects(iter->getPtr()); mIndex.erase(iter->getPtr().mRef); iter = mActors.erase(iter); } else ++iter; } } void Actors::predictAndAvoidCollisions(float duration) const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; const float minGap = 10.f; const float maxDistForPartialAvoiding = 200.f; const float maxDistForStrictAvoiding = 100.f; const float maxTimeToCheck = 2.0f; const bool giveWayWhenIdle = Settings::game().mNPCsGiveWay; const MWWorld::Ptr player = getPlayer(); const MWBase::World* const world = MWBase::Environment::get().getWorld(); for (const Actor& actor : mActors) { const MWWorld::Ptr& ptr = actor.getPtr(); if (ptr == player) continue; // Don't interfere with player controls. 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); 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. // Moving NPCs always should avoid collisions. // 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(); if (!aiSequence.isEmpty()) { const auto& package = aiSequence.getActivePackage(); if (package.getTypeId() == AiPackageTypeId::Follow) { shouldAvoidCollision = true; } else if (package.getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) { if (!static_cast(package).isStationary()) shouldGiveWay = true; } else if (package.getTypeId() == AiPackageTypeId::Combat || package.getTypeId() == AiPackageTypeId::Pursue) { currentTarget = package.getTarget(); shouldAvoidCollision = isMoving; shouldTurnToApproachingActor = false; } } if (!shouldAvoidCollision && !shouldGiveWay) continue; 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 = timeToCheck; osg::Vec2f movementCorrection(0, 0); float angleToApproachingActor = 0; // Iterate through all other actors and predict collisions. for (const Actor& otherActor : mActors) { const MWWorld::Ptr& otherPtr = otherActor.getPtr(); if (otherPtr == ptr || otherPtr == currentTarget) continue; 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) continue; // Don't check for a collision if vertical distance is greater then the actor's height. if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) continue; const osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() * otherPtr.getClass().getMaxSpeed(otherPtr); const 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 + halfExtents.x() + otherHalfExtents.x(); collisionDist = std::min(collisionDist, relPos.length()); // Find the earliest `t` when |relPos + relSpeed * t| == 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. const float t = (-vr - std::sqrt(Dh)) / v2; if (t < 0 || t > timeToCollision) continue; // Check visibility and awareness last as it's expensive. if (!MWBase::Environment::get().getWorld()->getLOS(otherPtr, ptr)) continue; if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(otherPtr, ptr)) continue; timeToCollision = t; angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); 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 < 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. newMovement.y() = std::max(newMovement.y(), 0.f); newMovement.normalize(); if (isMoving) newMovement *= origMovement.length(); // Keep the original speed. movement.mPosition[0] = newMovement.x(); movement.mPosition[1] = newMovement.y(); if (shouldTurnToApproachingActor) zTurn(ptr, angleToApproachingActor); } } } void Actors::update(float duration, bool paused) { if (!paused) { const float updateEquippedLightInterval = 1.0f; 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* const world = MWBase::Environment::get().getWorld(); const bool showTorches = world->useTorches(); const MWWorld::Ptr player = getPlayer(); const osg::Vec3f playerPos = player.getRefData().getPosition().asVec3(); /// \todo move update logic to Actor class where appropriate SidingCache cachedAllies{ *this, true }; // will be filled as engageCombat iterates 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); if (!playerHitAttemptActor.isInCell()) player.getClass().getCreatureStats(player).setHitAttemptActorId(-1); } const int actorsProcessingRange = Settings::game().mActorsProcessingRange; // AI and magic effects update for (Actor& actor : mActors) { const bool isPlayer = actor.getPtr() == player; CharacterController& ctrl = actor.getCharacterController(); MWBase::LuaManager::ActorControls* luaControls = MWBase::Environment::get().getLuaManager()->getActorControls(actor.getPtr()); const float distSqr = (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2(); // AI processing is only done within given distance to the player. 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)) { 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); } const Misc::TimerStatus engageCombatTimerStatus = actor.updateEngageCombatTimer(duration); // For dead actors we need to update looping spell particles if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { // They can be added during the death animation if (!actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDeathAnimationFinished()) adjustMagicEffects(actor.getPtr(), duration); ctrl.updateContinuousVfx(); } else { 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(); 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 } if (aiActive && inProcessingRange) { if (engageCombatTimerStatus == Misc::TimerStatus::Elapsed) { if (!isPlayer) adjustCommandedActor(actor.getPtr()); for (const Actor& otherActor : mActors) { if (otherActor.getPtr() == actor.getPtr() || isPlayer) // player is not AI-controlled continue; engageCombat( actor.getPtr(), otherActor.getPtr(), cachedAllies, otherActor.getPtr() == player); } } if (mTimerUpdateHeadTrack == 0) updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl); if (actor.getPtr().getClass().isNpc() && !isPlayer) updateCrimePursuit(actor.getPtr(), duration, cachedAllies); if (!isPlayer) { CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); if (isConscious(actor.getPtr()) && !(luaControls && luaControls->mDisableAI)) { stats.getAiSequence().execute(actor.getPtr(), ctrl, duration); updateGreetingState(actor.getPtr(), actor, mTimerUpdateHello > 0); playIdleDialogue(actor.getPtr()); updateMovementSpeed(actor.getPtr()); } } } else if (aiActive && !isPlayer && isConscious(actor.getPtr()) && !(luaControls && luaControls->mDisableAI)) { CreatureStats& stats = actor.getPtr().getClass().getCreatureStats(actor.getPtr()); stats.getAiSequence().execute(actor.getPtr(), ctrl, duration, /*outOfRange*/ true); } if (inProcessingRange && actor.getPtr().getClass().isNpc()) { // 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); } } if (Settings::game().mNPCsAvoidCollisions) predictAndAvoidCollisions(duration); mTimerUpdateHeadTrack += duration; mTimerUpdateEquippedLight += duration; mTimerUpdateHello += duration; mTimerDisposeSummonsCorpses += duration; // Animation/movement update CharacterController* playerCharacter = nullptr; for (Actor& actor : mActors) { 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(actor.getPtr()) && !stats.isParalyzed()) { MWMechanics::AiSequence& seq = stats.getAiSequence(); alwaysActive = !seq.isEmpty() && seq.getActivePackage().alwaysActive(); } 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 = actor.getCharacterController(); ctrl.setActive(active); if (!inRange) { actor.getPtr().getRefData().getBaseNode()->setNodeMask(0); world->setActorActive(actor.getPtr(), false); continue; } 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 (isPlayer) { playerCharacter = &ctrl; continue; } 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(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 (const Actor& actor : mActors) { 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. if (stats.getKnockedDown() && !stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { // Start it for one frame if necessary stats.setKnockedDownOneFrame(true); } else if (stats.getKnockedDownOneFrame() && !stats.getKnockedDownOverOneFrame()) { // Turn off KnockedOutOneframe stats.setKnockedDownOneFrame(false); stats.setKnockedDownOverOneFrame(true); } } killDeadActors(); updateSneaking(playerCharacter, duration); } } void Actors::notifyDied(const MWWorld::Ptr& actor) { actor.getClass().getCreatureStats(actor).notifyDied(); ++mDeathCount[actor.getCellRef().getRefId()]; MWBase::Environment::get().getLuaManager()->actorDied(actor); } void Actors::resurrect(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { if (iter->second->getCharacterController().isDead()) { // Actor has been resurrected. Notify the CharacterController and re-enable collision. MWBase::Environment::get().getWorld()->enableActorCollision(iter->second->getPtr(), true); iter->second->getCharacterController().resurrect(); } } } void Actors::killDeadActors() { for (Actor& actor : mActors) { const MWWorld::Class& cls = actor.getPtr().getClass(); CreatureStats& stats = cls.getCreatureStats(actor.getPtr()); if (!stats.isDead()) continue; 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(actor.getPtr(), ESM::RefId::stringRefId("hit")); // Apply soultrap if (actor.getPtr().getType() == ESM::Creature::sRecordId) soulTrap(actor.getPtr()); if (cls.isEssential(actor.getPtr())) MWBase::Environment::get().getWindowManager()->messageBox("#{sKilledEssential}"); } else if (killResult == CharacterController::Result_DeathAnimJustFinished) { 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 const float vampirism = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Vampirism).getMagnitude(); stats.getActiveSpells().clear(actor.getPtr()); // Make sure spell effects are removed purgeSpellEffects(stats.getActorId()); stats.getMagicEffects().add(ESM::MagicEffect::Vampirism, vampirism); if (isPlayer) { // 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(actor.getPtr(), false); } } } } void Actors::cleanupSummonedCreature(MWMechanics::CreatureStats& casterStats, int creatureActorId) const { 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().getESMStore()->get().search( ESM::RefId::stringRefId("VFX_Summon_End")); if (fx) MWBase::Environment::get().getWorld()->spawnEffect( Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)), "", ptr.getRefData().getPosition().asVec3()); // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); auto& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); } else if (creatureActorId != -1) { // We didn't find the creature. It's probably in an inactive cell. // Add to graveyard so we can delete it when the cell becomes active. std::vector& graveyard = casterStats.getSummonedCreatureGraveyard(); graveyard.push_back(creatureActorId); } purgeSpellEffects(creatureActorId); } void Actors::purgeSpellEffects(int casterActorId) const { for (const Actor& actor : mActors) { MWMechanics::ActiveSpells& spells = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getActiveSpells(); spells.purge(actor.getPtr(), casterActorId); } } void Actors::rest(double hours, bool sleep) const { float duration = hours * 3600.f; 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 (const Actor& actor : mActors) { if (actor.getPtr().getClass().getCreatureStats(actor.getPtr()).isDead()) { adjustMagicEffects(actor.getPtr(), duration); continue; } if (!sleep || actor.getPtr() == player) restoreDynamicStats(actor.getPtr(), hours, sleep); if ((!actor.getPtr().getRefData().getBaseNode()) || (playerPos - actor.getPtr().getRefData().getPosition().asVec3()).length2() > actorsProcessingRange * actorsProcessingRange) continue; // Get rid of effects pending removal so they are not applied when resting updateMagicEffects(actor.getPtr()); adjustMagicEffects(actor.getPtr(), duration); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(actor.getPtr()); if (animation) { animation->removeEffects(); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor.getPtr()); } } fastForwardAi(); } void Actors::updateSneaking(CharacterController* ctrl, float duration) { if (!ctrl) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } const MWWorld::Ptr player = getPlayer(); if (!MWBase::Environment::get().getMechanicsManager()->isSneaking(player)) { MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); return; } 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 (mSneakTimer >= fSneakUseDelay) mSneakTimer = 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; const osg::Vec3f position(player.getRefData().getPosition().asVec3()); const float radius = std::min(fSneakUseDist, Settings::game().mActorsProcessingRange); getObjectsInRange(position, radius, 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)) { detected = true; avoidedNotice = false; MWBase::Environment::get().getWindowManager()->setSneakVisibility(false); break; } else { avoidedNotice = true; } } } if (mSneakSkillTimer >= fSneakUseDelay) mSneakSkillTimer = 0.f; if (avoidedNotice && mSneakSkillTimer == 0.f) player.getClass().skillUsageSucceeded(player, ESM::Skill::Sneak, ESM::Skill::Sneak_AvoidNotice); if (!detected) MWBase::Environment::get().getWindowManager()->setSneakVisibility(true); } mSneakTimer += duration; mSneakSkillTimer += duration; } int Actors::getHoursToRest(const MWWorld::Ptr& ptr) const { const auto [healthPerHour, magickaPerHour] = getRestorationPerHourOfSleep(ptr); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); const bool stunted = stats.getMagicEffects().getOrDefault(ESM::MagicEffect::StuntedMagicka).getMagnitude() > 0; 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; return static_cast(std::ceil(std::max(1.f, std::max(healthHours, magickaHours)))); } int Actors::countDeaths(const ESM::RefId& id) const { const auto iter = mDeathCount.find(id); if (iter != mDeathCount.end()) return iter->second; return 0; } void Actors::forceStateUpdate(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) iter->second->getCharacterController().forceStateUpdate(); } bool Actors::playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, uint32_t number, bool scripted) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) { return iter->second->getCharacterController().playGroup(groupName, mode, number, scripted); } else { Log(Debug::Warning) << "Warning: Actors::playAnimationGroup: Unable to find " << ptr.getCellRef().getRefId(); return false; } } 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, std::string_view groupName) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) return iter->second->getCharacterController().isAnimPlaying(groupName); return false; } bool Actors::checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) return iter->second->getCharacterController().isScriptedAnimPlaying(); return false; } void Actors::persistAnimationStates() const { for (const Actor& actor : mActors) actor.getCharacterController().persistAnimationState(); } void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) { 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) const { for (const Actor& actor : mActors) { if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) out.push_back(actor.getPtr()); } } bool Actors::isAnyObjectInRange(const osg::Vec3f& position, float radius) const { for (const Actor& actor : mActors) { if ((actor.getPtr().getRefData().getPosition().asVec3() - position).length2() <= radius * radius) return true; } return false; } std::vector Actors::getActorsSidingWith(const MWWorld::Ptr& actorPtr, bool excludeInfighting) const { std::vector list; list.push_back(actorPtr); for (const Actor& actor : mActors) { const MWWorld::Ptr& iteratedActor = actor.getPtr(); if (iteratedActor == getPlayer()) continue; const bool sameActor = (iteratedActor == actorPtr); 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 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->targetIs(actorPtr)) { list.push_back(iteratedActor); } break; } else if (package->getTypeId() > AiPackageTypeId::Wander && package->getTypeId() <= AiPackageTypeId::Activate) // Don't count "fake" package types break; } } return list; } std::vector Actors::getActorsFollowing(const MWWorld::Ptr& actorPtr) const { 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) 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, 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::vector Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const { 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) const { std::map map; 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::vector Actors::getActorsFighting(const MWWorld::Ptr& actor) const { std::vector list; std::vector 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); if (stats.isDead()) continue; if (stats.getAiSequence().isInCombat(actor)) list.push_back(neighbor); } return list; } std::vector Actors::getEnemiesNearby(const MWWorld::Ptr& actor) const { std::vector list; std::vector neighbors; osg::Vec3f position(actor.getRefData().getPosition().asVec3()); getObjectsInRange(position, Settings::game().mActorsProcessingRange, neighbors); std::set followers; getActorsFollowing(actor, followers); for (const MWWorld::Ptr& neighbor : neighbors) { 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(); 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 { writer.startRecord(ESM::REC_DCOU); for (const auto& [id, count] : mDeathCount) { writer.writeHNRefId("ID__", id); writer.writeHNT("COUN", count); } writer.endRecord(ESM::REC_DCOU); } void Actors::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_DCOU) { while (reader.isNextSub("ID__")) { ESM::RefId id = reader.getRefId(); int count; reader.getHNT(count, "COUN"); if (MWBase::Environment::get().getESMStore()->find(id)) mDeathCount[id] = count; } } } void Actors::clear() { mIndex.clear(); mActors.clear(); mDeathCount.clear(); } void Actors::updateMagicEffects(const MWWorld::Ptr& ptr) const { adjustMagicEffects(ptr, 0.f); } bool Actors::isReadyToBlock(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isReadyToBlock(); } bool Actors::isCastingSpell(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isCastingSpell(); } bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->getCharacterController().isAttackingOrSpell(); } int Actors::getGreetingTimer(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return 0; return it->second->getGreetingTimer(); } float Actors::getAngleToPlayer(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return 0.f; return it->second->getAngleToPlayer(); } GreetingState Actors::getGreetingState(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return Greet_None; return it->second->getGreetingState(); } bool Actors::isTurningToPlayer(const MWWorld::Ptr& ptr) const { const auto it = mIndex.find(ptr.mRef); if (it == mIndex.end()) return false; return it->second->isTurningToPlayer(); } void Actors::fastForwardAi() const { if (!MWBase::Environment::get().getMechanicsManager()->isAIActive()) return; for (auto it = mActors.begin(); it != mActors.end();) { 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; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/actors.hpp000066400000000000000000000217231503074453300231770ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_ACTORS_H #define GAME_MWMECHANICS_ACTORS_H #include #include #include #include #include #include "actor.hpp" namespace ESM { class ESMReader; class ESMWriter; } namespace osg { class Vec3f; } namespace Loading { class Listener; } namespace MWWorld { class Ptr; class CellStore; } namespace MWMechanics { class Actor; class CharacterController; class CreatureStats; class SidingCache; class Actors { 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 notifyDied(const MWWorld::Ptr& actor); /// 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; /// 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 addActor(const MWWorld::Ptr& ptr, bool updateImmediately = false); ///< Register an actor for stats management /// /// \note Dead actors are ignored. 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 resurrect(const MWWorld::Ptr& ptr) const; void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) const; void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const; ///< Updates an actor with a new Ptr void dropActors(const MWWorld::CellStore* cellStore, const MWWorld::Ptr& ignore); ///< Deregister all actors (except for \a ignore) in the given cell. void update(float duration, bool paused); ///< Update actor stats and store desired velocity vectors in \a movement 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. /// Removes an actor from combat and makes all of their allies stop fighting the actor's targets void stopCombat(const MWWorld::Ptr& ptr) const; 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 rest(double hours, bool sleep) const; ///< Update actors while the player is waiting or sleeping. void updateSneaking(CharacterController* ctrl, float duration); ///< Update the sneaking indicator state according to the given player character controller. void restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) const; int getHoursToRest(const MWWorld::Ptr& ptr) const; ///< Calculate how many hours the given actor needs to rest in order to be fully healed void fastForwardAi() const; ///< Simulate the passing of time int countDeaths(const ESM::RefId& id) const; ///< Return the number of deaths for actors with the given ID. bool isAttackPreparing(const MWWorld::Ptr& ptr) const; bool isRunning(const MWWorld::Ptr& ptr) const; bool isSneaking(const MWWorld::Ptr& ptr) const; void forceStateUpdate(const MWWorld::Ptr& ptr) const; 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, std::string_view groupName) const; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; void persistAnimationStates() const; void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; bool isAnyObjectInRange(const osg::Vec3f& position, float radius) const; void cleanupSummonedCreature(CreatureStats& casterStats, int creatureActorId) const; /// 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; /// 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; /// 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; /// 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; /// Unlike getActorsFighting, also returns actors that *would* fight the given actor if they saw him. std::vector getEnemiesNearby(const MWWorld::Ptr& actor) const; void write(ESM::ESMWriter& writer, Loading::Listener& listener) const; void readRecord(ESM::ESMReader& reader, uint32_t type); void clear(); // Clear death counter bool isCastingSpell(const MWWorld::Ptr& ptr) const; bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; 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; 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" void updateVisibility(const MWWorld::Ptr& ptr, CharacterController& ctrl) const; void adjustMagicEffects(const MWWorld::Ptr& creature, float duration) const; void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const; void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const; void killDeadActors(); void purgeSpellEffects(int casterActorId) const; void predictAndAvoidCollisions(float duration) 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, SidingCache& cachedAllies, bool againstPlayer) const; }; 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/actorutil.cpp000066400000000000000000000026131503074453300237020ustar00rootroot00000000000000#include "actorutil.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() { return MWBase::Environment::get().getWorld()->getPlayerPtr(); } bool isPlayerInCombat() { return MWBase::Environment::get().getWorld()->getPlayer().isInCombat(); } bool canActorMoveByZAxis(const MWWorld::Ptr& actor) { MWBase::World* world = MWBase::Environment::get().getWorld(); return (actor.getClass().canSwim(actor) && world->isSwimming(actor)) || world->isFlying(actor); } bool hasWaterWalking(const MWWorld::Ptr& actor) { const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); 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); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/actorutil.hpp000066400000000000000000000005751503074453300237140ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_ACTORUTIL_H #define OPENMW_MWMECHANICS_ACTORUTIL_H namespace MWWorld { class Ptr; } namespace MWMechanics { MWWorld::Ptr getPlayer(); bool isPlayerInCombat(); bool canActorMoveByZAxis(const MWWorld::Ptr& actor); bool hasWaterWalking(const MWWorld::Ptr& actor); bool isTargetMagicallyHidden(const MWWorld::Ptr& actor); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiactivate.cpp000066400000000000000000000047461503074453300240170ustar00rootroot00000000000000#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) { } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiactivate.hpp000066400000000000000000000021671503074453300240170ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiavoiddoor.cpp000066400000000000000000000055221503074453300241760ustar00rootroot00000000000000#include "aiavoiddoor.hpp" #include #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 "steering.hpp" static const int MAX_DIRECTIONS = 4; MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::ConstPtr& doorPtr) : mDuration(1) , mDoorPtr(doorPtr) , mDirection(0) { } 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 mLastPos = pos.asVec3(); mDuration -= duration; // Update timer if (mDuration < 0) { if (isStuck(pos.asVec3())) { adjustDirection(); 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 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))) actor.getClass().getMovementSettings(actor).mPosition[1] = 1; else actor.getClass().getMovementSettings(actor).mPosition[1] = 0; actor.getClass().getMovementSettings(actor).mPosition[0] = 0; // Make all nearby actors also avoid the door std::vector actors; MWBase::Environment::get().getMechanicsManager()->getActorsInRange(pos.asVec3(), 100, actors); for (auto& neighbor : actors) { if (neighbor == getPlayer()) continue; MWMechanics::AiSequence& seq = neighbor.getClass().getCreatureStats(neighbor).getAiSequence(); if (seq.getTypeId() != MWMechanics::AiPackageTypeId::AvoidDoor) seq.stack(MWMechanics::AiAvoidDoor(mDoorPtr), neighbor); } return false; } bool MWMechanics::AiAvoidDoor::isStuck(const osg::Vec3f& actorPos) const { return (actorPos - mLastPos).length2() < 10 * 10; } void MWMechanics::AiAvoidDoor::adjustDirection() { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); mDirection = Misc::Rng::rollDice(MAX_DIRECTIONS, prng); } float MWMechanics::AiAvoidDoor::getAdjustedAngle() const { return 2 * osg::PI / MAX_DIRECTIONS * mDirection; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiavoiddoor.hpp000066400000000000000000000025101503074453300241750ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIAVOIDDOOR_H #define GAME_MWMECHANICS_AIAVOIDDOOR_H #include "typedaipackage.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 **/ class AiAvoidDoor final : public TypedAiPackage { 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; 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; } private: float mDuration; const MWWorld::ConstPtr mDoorPtr; osg::Vec3f mLastPos; int mDirection; bool isStuck(const osg::Vec3f& actorPos) const; void adjustDirection(); float getAdjustedAngle() const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aibreathe.cpp000066400000000000000000000017431503074453300236230ustar00rootroot00000000000000#include "aibreathe.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "npcstats.hpp" #include "movement.hpp" #include "steering.hpp" bool MWMechanics::AiBreathe::execute( const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { static const float fHoldBreathTime = MWBase::Environment::get().getESMStore()->get().find("fHoldBreathTime")->mValue.getFloat(); const MWWorld::Class& actorClass = actor.getClass(); if (actorClass.isNpc()) { if (actorClass.getNpcStats(actor).getTimeToStartDrowning() < fHoldBreathTime / 2) { actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; smoothTurn(actor, static_cast(-osg::PI_2), 0); return false; } } return true; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aibreathe.hpp000066400000000000000000000015231503074453300236240ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIBREATHE_H #define GAME_MWMECHANICS_AIBREATHE_H #include "typedaipackage.hpp" namespace MWMechanics { /// \brief AiPackage to have an actor resurface to breathe // 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; 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; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicast.cpp000066400000000000000000000060721503074453300231430ustar00rootroot00000000000000#include "aicast.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "aicombataction.hpp" #include "character.hpp" #include "steering.hpp" namespace MWMechanics { namespace { float getInitialDistance(const ESM::RefId& spellId) { ActionSpell action = ActionSpell(spellId); bool isRanged; return action.getCombatRange(isRanged); } } } 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) { MWWorld::Ptr target; if (actor.getCellRef().getRefId() == mTargetId) { // If the target has the same ID as caster, consider that actor casts spell with Self range. target = actor; } else { target = getTarget(); if (target.isEmpty()) return true; if (!mScripted && !pathTo(actor, target.getRefData().getPosition().asVec3(), duration, characterController.getSupportedMovementDirections(), mDistance)) { return false; } } osg::Vec3f targetPos = target.getRefData().getPosition().asVec3(); // If the target of an on-target spell is an actor that is not the caster // the target position must be adjusted so that it's not casted at the actor's feet. if (target != actor && target.getClass().isActor()) { osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target); targetPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; } osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor); actorPos.z() += halfExtents.z() * 2 * Constants::TorsoHeight; osg::Vec3f dir = targetPos - actorPos; bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); if (!turned) return false; // Check if the actor is already casting another spell bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); if (isCasting && !mCasting) return false; if (!mCasting) { MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted); mCasting = true; return false; } // Finish package, if actor finished spellcasting return !isCasting; } MWWorld::Ptr MWMechanics::AiCast::getTarget() const { MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetId, false); return target; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicast.hpp000066400000000000000000000022221503074453300231410ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AICAST_H #define GAME_MWMECHANICS_AICAST_H #include "typedaipackage.hpp" #include namespace MWWorld { class Ptr; } namespace MWMechanics { /// AiPackage which makes an actor to cast given spell. 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicombat.cpp000066400000000000000000001050401503074453300234510ustar00rootroot00000000000000#include "aicombat.hpp" #include #include #include #include #include #include #include "../mwphysics/raycasting.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "actorutil.hpp" #include "aicombataction.hpp" #include "character.hpp" #include "combat.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "pathgrid.hpp" #include "steering.hpp" #include "weapontype.hpp" namespace { // 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); } namespace MWMechanics { AiCombat::AiCombat(const MWWorld::Ptr& actor) { mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } AiCombat::AiCombat(const ESM::AiSequence::AiCombat* combat) { mTargetActorId = combat->mTargetActorId; } void AiCombat::init() {} /* * Current AiCombat movement states (as of 0.29.0), ignoring the details of the * attack states such as CombatMove, Strike and ReadyToAttack: * * +----(within strike range)----->attack--(beyond strike range)-->follow * | | ^ | | * | | | | | * pursue<---(beyond follow range)-----+ +----(within strike range)---+ | * ^ | * | | * +-------------------------(beyond follow range)--------------------+ * * * Below diagram is high level only, the code detail is a little different * (but including those detail will just complicate the diagram w/o adding much) * * +----------(same)-------------->attack---------(same)---------->follow * | |^^ ||| * | ||| ||| * | +--(same)-----------------+|+----------(same)------------+|| * | | | || * | | | (in range) || * | <---+ (too far) | || * pursue<-------------------------[door open]<-----+ || * ^^^ | || * ||| | || * ||+----------evade-----+ | || * || | [closed door] | || * |+----> maybe stuck, check --------------> back up, check door || * | ^ | ^ | ^ || * | | | | | | || * | | +---+ +---+ || * | +-------------------------------------------------------+| * | | * +---------------------------(same)---------------------------------+ * * FIXME: * * The new scheme is way too complicated, should really be implemented as a * proper state machine. * * TODO: * * Use the observer pattern to coordinate attacks, provide intelligence on * whether the target was hit, etc. */ bool AiCombat::execute( const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { // get or create temporary storage AiCombatStorage& storage = state.get(); // General description if (actor.getClass().getCreatureStats(actor).isDead()) return true; MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); if (target.isEmpty()) return true; 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. return true; if (!storage.isFleeing()) { 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. updateLOS(actor, target, duration, storage); 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, characterController.getSupportedMovementDirections(), targetReachedTolerance); if (is_target_reached) storage.mReadyToAttack = true; } storage.updateCombatMove(duration); storage.mRotateMove = false; if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage); if (storage.mRotateMove) return false; storage.updateAttack(actor, characterController); } else { updateFleeing(actor, target, duration, characterController.getSupportedMovementDirections(), storage); } storage.mActionCooldown -= duration; if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; return attack(actor, target, storage, 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) { currentCell = actor.getCell(); } bool forceFlee = false; if (!canFight(actor, target)) { storage.stopAttack(); 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 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()))) forceFlee = true; else // Otherwise end combat return true; } const MWWorld::Class& actorClass = actor.getClass(); actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); float& actionCooldown = storage.mActionCooldown; std::unique_ptr& currentAction = storage.mCurrentAction; if (!forceFlee) { if (actionCooldown > 0) return false; if (characterController.readyToPrepareAttack()) { currentAction = prepareNextAction(actor, target); actionCooldown = currentAction->getActionCooldown(); } } else { currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); } if (!currentAction) return false; if (storage.isFleeing() != currentAction->isFleeing()) { if (currentAction->isFleeing()) { storage.startFleeing(); MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("flee")); return false; } else storage.stopFleeing(); } bool isRangedCombat = false; float& rangeAttack = storage.mAttackRange; rangeAttack = currentAction->getCombatRange(isRangedCombat); // Get weapon characteristics const ESM::Weapon* weapon = currentAction->getWeapon(); ESM::Position pos = actor.getRefData().getPosition(); const osg::Vec3f vActorPos(pos.asVec3()); const osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); 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); storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir); } else { 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.mLastTargetPos = vTargetPos; if (storage.mReadyToAttack) { storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack 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 // (door is opened between actor and target) or target position has changed and current custom destination // is not good enough to attack target. if (storage.mCurrentAction->isAttackingOrSpell() && ((!storage.mReadyToAttack && !mPathFinder.isPathConstructed()) || (storage.mUseCustomDestination && (storage.mCustomDestination - vTargetPos).length() > rangeAttack))) { const MWBase::World* world = MWBase::Environment::get().getWorld(); // Try to build path to the target. 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 = 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, agentBounds, navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); if (mPathFinder.isPathConstructed()) { // If path to that point is found use it as custom destination. storage.mCustomDestination = *hit; storage.mUseCustomDestination = true; } } if (!mPathFinder.isPathConstructed()) { storage.mUseCustomDestination = false; storage.stopAttack(); actor.getClass().getCreatureStats(actor).setAttackingOrSpell(false); currentAction = std::make_unique(); actionCooldown = currentAction->getActionCooldown(); storage.startFleeing(); MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("flee")); } } else { storage.mUseCustomDestination = false; } } return false; } 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) { storage.mLOS = MWBase::Environment::get().getWorld()->getLOS(actor, target); storage.mUpdateLOSTimer = LOS_UPDATE_DURATION; } else storage.mUpdateLOSTimer -= duration; } 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; updateLOS(actor, target, duration, storage); AiCombatStorage::FleeState& state = storage.mFleeState; switch (state) { case AiCombatStorage::FleeState_None: 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)) { const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellVariant); bool runFallback = true; if (pathgrid != nullptr && !pathgrid->mPoints.empty() && !actor.getClass().isPureWaterCreature(actor)) { ESM::Pathgrid::PointList points; const Misc::CoordinateConverter coords = Misc::makeCoordinateConverter(*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)) { points.push_back(pathgrid->mPoints[static_cast(i)]); } } 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); runFallback = false; } } if (runFallback) { state = AiCombatStorage::FleeState_RunBlindly; storage.mFleeBlindRunTimer = 0.0f; } } } break; case AiCombatStorage::FleeState_RunBlindly: { // 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; } 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)) { state = AiCombatStorage::FleeState_Idle; } } break; }; } void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) { // apply combat movement float deltaAngle = storage.mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]; osg::Vec2f movement = Misc::rotateVec2f( osg::Vec2f(storage.mMovement.mPosition[0], storage.mMovement.mPosition[1]), -deltaAngle); MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); actorMovementSettings.mPosition[0] = movement.x(); actorMovementSettings.mPosition[1] = movement.y(); actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; rotateActorOnAxis(actor, 2, actorMovementSettings, storage); rotateActorOnAxis(actor, 0, actorMovementSettings, 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]; storage.mRotateMove = !smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const { 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 { auto combat = std::make_unique(); combat->mTargetActorId = mTargetActorId; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Combat; 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) { 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(); if (targetClass.hasInventoryStore(target)) { int weapType = ESM::Weapon::None; MWWorld::ContainerStoreIterator weaponSlot = MWMechanics::getActiveWeapon(target, &weapType); if (weapType > ESM::Weapon::None) targetWeapon = *weaponSlot; } bool targetUsesRanged = false; float rangeAttackOfTarget = ActionWeapon(targetWeapon).getCombatRange(targetUsesRanged); if (mMovement.mPosition[0]) { mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(prng); mCombatMove = true; } // 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 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 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; // 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. // This approach allows us to detect small obstacles (e.g. crates) and curved walls. 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 destination = source + fallbackDirection * (halfExtents.y() + 16); const auto* rayCasting = MWBase::Environment::get().getWorld()->getRayCasting(); bool isObstacleDetected = rayCasting->castRay(source, destination, mask).mHit; if (isObstacleDetected) return; // Check if there is nothing behind - probably actor is near cliff. // A current approach: cast ray 1.5-yard ray down in 1.5 yard behind actor from 35% of actor's height. // 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 = !rayCasting->castRay(source, destination, mask).mHit; if (isCliffDetected) return; mMovement.mPosition[1] = -1; } } void AiCombatStorage::updateCombatMove(float duration) { if (mCombatMove) { mTimerCombatMove -= duration; if (mTimerCombatMove <= 0) { stopCombatMove(); } } } void AiCombatStorage::stopCombatMove() { mTimerCombatMove = 0; mMovement.mPosition[0] = 0; mCombatMove = false; } 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 actor.getClass().getCreatureStats(actor).setAttackingOrSpell(true); if (!distantCombat) characterController.setAIAttackType(chooseBestAttack(weapon)); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); mStrength = Misc::Rng::rollClosedProbability(prng); const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); float baseDelay = store.get().find("fCombatDelayCreature")->mValue.getFloat(); if (actor.getClass().isNpc()) { baseDelay = store.get().find("fCombatDelayNPC")->mValue.getFloat(); } if (canShout) { // 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(prng), baseDelay + 0.9); } else mAttackCooldown -= AI_REACTION_TIME; } } void AiCombatStorage::updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController) { if (mAttack) { float attackStrength = characterController.calculateWindUp(); mAttack = !characterController.readyToPrepareAttack() && attackStrength < mStrength && attackStrength != -1.f; } actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mAttack); } void AiCombatStorage::stopAttack() { mMovement.mPosition[0] = 0; mMovement.mPosition[1] = 0; mMovement.mPosition[2] = 0; mReadyToAttack = false; mAttack = false; } void AiCombatStorage::startFleeing() { stopFleeing(); mFleeState = FleeState_Idle; } void AiCombatStorage::stopFleeing() { mMovement.mPosition[0] = 0; mMovement.mPosition[1] = 0; mMovement.mPosition[2] = 0; mFleeState = FleeState_None; mFleeDest = ESM::Pathgrid::Point(0, 0, 0); } bool AiCombatStorage::isFleeing() const { return mFleeState != FleeState_None; } } namespace { std::string_view chooseBestAttack(const ESM::Weapon* weapon) { 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(); } 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(); // 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 = 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 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 vPerpToDir = vDirToTarget ^ osg::Vec3f(0, 0, 1); // cross product vPerpToDir.normalize(); osg::Vec3f vDirToTargetNormalized = vDirToTarget; vDirToTargetNormalized.normalize(); // dot product float velPerp = vTargetMoveDir * vPerpToDir; float velDir = vTargetMoveDir * vDirToTargetNormalized; // 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 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 return vDirToTarget + vTargetMoveDir * t_collision; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicombat.hpp000066400000000000000000000076411503074453300234660ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AICOMBAT_H #define GAME_MWMECHANICS_AICOMBAT_H #include "aitemporarybase.hpp" #include "typedaipackage.hpp" #include "../mwworld/cellstore.hpp" // for Doors #include "aitimer.hpp" #include "movement.hpp" namespace ESM { namespace AiSequence { struct AiCombat; } } namespace MWMechanics { class Action; /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. struct AiCombatStorage : AiTemporaryBase { float mAttackCooldown; AiReactionTimer mReaction; float mTimerCombatMove; bool mReadyToAttack; bool mAttack; float mAttackRange; bool mCombatMove; bool mRotateMove; osg::Vec3f mLastTargetPos; const MWWorld::CellStore* mCell; std::unique_ptr mCurrentAction; float mActionCooldown; float mStrength; bool mForceNoShortcut; ESM::Position mShortcutFailPos; MWMechanics::Movement mMovement; enum FleeState { FleeState_None, FleeState_Idle, FleeState_RunBlindly, FleeState_RunToDestination }; FleeState mFleeState; bool mLOS; float mUpdateLOSTimer; float mFleeBlindRunTimer; ESM::Pathgrid::Point mFleeDest; bool mUseCustomDestination; osg::Vec3f mCustomDestination; 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, bool canShout); void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController); void stopAttack(); void startFleeing(); void stopFleeing(); 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); explicit AiCombat(const ESM::AiSequence::AiCombat* combat); void init(); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; 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; } /// Returns target ID MWWorld::Ptr getTarget() 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); 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, 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicombataction.cpp000066400000000000000000000527531503074453300246630ustar00rootroot00000000000000#include "aicombataction.hpp" #include #include #include "../mwbase/environment.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 "actorutil.hpp" #include "combat.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() .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); if (rangeTypes & RangeTypes::Touch) { return fCombatDistance; } return distance * 4; } void ActionSpell::prepare(const MWWorld::Ptr& actor) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(mSpellId); 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().getESMStore()->get().find(mSpellId); MWBase::Environment::get().getWorld()->preloadEffects(&spell->mEffects); } float ActionSpell::getCombatRange(bool& isRanged) const { 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) { actor.getClass().getCreatureStats(actor).getSpells().setSelectedSpell(ESM::RefId()); actor.getClass().getInventoryStore(actor).setSelectedEnchantItem(mItem); actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Spell); } float ActionEnchantedItem::getCombatRange(bool& isRanged) const { 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); return suggestCombatRange(types); } float ActionPotion::getCombatRange(bool& isRanged) const { // Distance doesn't matter since this action has no animation // If we want to back away slightly to avoid enemy hits, we should set isRanged to "true" return 600.f; } void ActionPotion::prepare(const MWWorld::Ptr& actor) { actor.getClass().consume(mPotion, 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); else { MWWorld::ActionEquip equip(mWeapon); equip.execute(actor); } if (!mAmmunition.isEmpty()) { MWWorld::ActionEquip equip(mAmmunition); equip.execute(actor); } } actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Weapon); } float ActionWeapon::getCombatRange(bool& isRanged) const { isRanged = false; 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() .getESMStore() ->get() .find("fHandToHandReach") ->mValue.getFloat(); return fHandToHandReach * fCombatDistance; } const ESM::Weapon* weapon = mWeapon.get()->mBase; if (MWMechanics::getWeaponType(weapon->mData.mType)->mWeaponClass != ESM::WeaponType::Melee) { isRanged = true; return fProjectileMaxSpeed; } else return weapon->mData.mReach * fCombatDistance; } const ESM::Weapon* ActionWeapon::getWeapon() const { if (mWeapon.isEmpty()) return nullptr; return mWeapon.get()->mBase; } 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::unique_ptr bestAction = std::make_unique(MWWorld::Ptr()); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { bestAction->prepare(actor); return bestAction; } const bool hasInventoryStore = actor.getClass().hasInventoryStore(actor); MWWorld::ContainerStore& store = actor.getClass().getContainerStore(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 = std::make_unique(*it); antiFleeRating = std::numeric_limits::max(); } } // 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 = std::make_unique(it); antiFleeRating = std::numeric_limits::max(); } } } if (hasInventoryStore) { MWWorld::Ptr bestArrow; float bestArrowRating = rateAmmo(actor, enemy, bestArrow, ESM::Weapon::Arrow); MWWorld::Ptr bestBolt; float bestBoltRating = rateAmmo(actor, enemy, bestBolt, ESM::Weapon::Bolt); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, -1, bestArrowRating, bestBoltRating); if (rating > bestActionRating) { const ESM::Weapon* weapon = it->get()->mBase; int ammotype = getWeaponType(weapon->mData.mType)->mAmmoType; MWWorld::Ptr ammo; if (ammotype == ESM::Weapon::Arrow) ammo = bestArrow; else if (ammotype == ESM::Weapon::Bolt) ammo = bestBolt; bestActionRating = rating; bestAction = std::make_unique(*it, ammo); antiFleeRating = vanillaRateWeaponAndAmmo(*it, ammo, actor, enemy); } } } for (const ESM::Spell* spell : spells) { float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; bestAction = std::make_unique(spell->mId); antiFleeRating = vanillaRateSpell(spell, actor, enemy); } } if (makeFleeDecision(actor, enemy, antiFleeRating)) bestAction = std::make_unique(); if (bestAction.get()) bestAction->prepare(actor); return bestAction; } float getBestActionRating(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { Spells& spells = actor.getClass().getCreatureStats(actor).getSpells(); float bestActionRating = 0.f; // Default to hand-to-hand combat if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { return bestActionRating; } if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateMagicItem(*it, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; } } float bestArrowRating = rateAmmo(actor, enemy, ESM::Weapon::Arrow); float bestBoltRating = rateAmmo(actor, enemy, ESM::Weapon::Bolt); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, -1, bestArrowRating, bestBoltRating); if (rating > bestActionRating) { bestActionRating = rating; } } } for (const ESM::Spell* spell : spells) { float rating = rateSpell(spell, actor, enemy); if (rating > bestActionRating) { bestActionRating = rating; } } return bestActionRating; } float getDistanceMinusHalfExtents(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool minusZDist) { osg::Vec3f actor1Pos = actor1.getRefData().getPosition().asVec3(); osg::Vec3f actor2Pos = actor2.getRefData().getPosition().asVec3(); float dist = (actor1Pos - actor2Pos).length(); 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()); } float getMaxAttackDistance(const MWWorld::Ptr& actor) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); const ESM::RefId& selectedSpellId = stats.getSpells().getSelectedSpell(); MWWorld::Ptr selectedEnchItem; MWWorld::Ptr activeWeapon, activeAmmo; if (actor.getClass().hasInventoryStore(actor)) { MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); MWWorld::ContainerStoreIterator item = invStore.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (item != invStore.end() && item.getType() == MWWorld::ContainerStore::Type_Weapon) activeWeapon = *item; item = invStore.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (item != invStore.end() && item.getType() == MWWorld::ContainerStore::Type_Weapon) activeAmmo = *item; if (invStore.getSelectedEnchantItem() != invStore.end()) selectedEnchItem = *invStore.getSelectedEnchantItem(); } float dist = 1.0f; if (activeWeapon.isEmpty() && !selectedSpellId.empty() && !selectedEnchItem.isEmpty()) { static const float fHandToHandReach = gmst.find("fHandToHandReach")->mValue.getFloat(); dist = fHandToHandReach; } else if (stats.getDrawState() == MWMechanics::DrawState::Spell) { dist = 1.0f; if (!selectedSpellId.empty()) { 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->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } } } else if (!selectedEnchItem.isEmpty()) { const ESM::RefId& enchId = selectedEnchItem.getClass().getEnchantment(selectedEnchItem); if (!enchId.empty()) { 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->mData.mRange == ESM::RT_Target) { const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get().find( effectIt->mData.mEffectID); dist = effect->mData.mSpeed; break; } } } } static const float fTargetSpellMaxSpeed = gmst.find("fTargetSpellMaxSpeed")->mValue.getFloat(); dist *= std::max(1000.0f, fTargetSpellMaxSpeed); } else if (!activeWeapon.isEmpty()) { const ESM::Weapon* esmWeap = activeWeapon.get()->mBase; if (MWMechanics::getWeaponType(esmWeap->mData.mType)->mWeaponClass != ESM::WeaponType::Melee) { static const float fTargetSpellMaxSpeed = gmst.find("fProjectileMaxSpeed")->mValue.getFloat(); dist = fTargetSpellMaxSpeed; if (!activeAmmo.isEmpty()) { const ESM::Weapon* esmAmmo = activeAmmo.get()->mBase; dist *= esmAmmo->mData.mSpeed; } } else if (esmWeap->mData.mReach > 1) { dist = esmWeap->mData.mReach; } } dist = (dist > 0.f) ? dist : 1.0f; static const float fCombatDistance = gmst.find("fCombatDistance")->mValue.getFloat(); static const float fCombatDistanceWerewolfMod = gmst.find("fCombatDistanceWerewolfMod")->mValue.getFloat(); float combatDistance = fCombatDistance; if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) combatDistance *= (fCombatDistanceWerewolfMod + 1.0f); if (dist < combatDistance) dist *= combatDistance; return dist; } bool canFight(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { ESM::Position actorPos = actor.getRefData().getPosition(); ESM::Position enemyPos = enemy.getRefData().getPosition(); if (isTargetMagicallyHidden(enemy) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(enemy, actor)) { return false; } if (actor.getClass().isPureWaterCreature(actor)) { if (!MWBase::Environment::get().getWorld()->isWading(enemy)) return false; } float atDist = getMaxAttackDistance(actor); 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)) { return false; } if (actor.getClass().isPureFlyingCreature(actor) || actor.getClass().isPureLandCreature(actor)) { if (MWBase::Environment::get().getWorld()->isSwimming(enemy)) return false; } if (actor.getClass().isBipedal(actor) || !actor.getClass().canFly(actor)) { 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(), enemy.getCell()->getCell()->getWorldSpace()))) return false; } } } } if (!actor.getClass().canWalk(actor) && !actor.getClass().isBipedal(actor)) return true; if (actor.getClass() .getCreatureStats(actor) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Levitate) .getMagnitude() > 0) return true; if (MWBase::Environment::get().getWorld()->isSwimming(actor)) return true; if (getDistanceMinusHalfExtents(actor, enemy, true) <= 0.0f) return false; return true; } float vanillaRateFlee(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy) { const CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); 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().getRatio(false); float rating = (1.0f - healthPercentage) * fAIFleeHealthMult + flee * fAIFleeFleeMult; static const int iWereWolfLevelToAttack = gmst.find("iWereWolfLevelToAttack")->mValue.getInteger(); if (actor.getClass().isNpc() && enemy.getClass().isNpc()) { if (enemy.getClass().getNpcStats(enemy).isWerewolf() && stats.getLevel() < iWereWolfLevelToAttack) { static const int iWereWolfFleeMod = gmst.find("iWereWolfFleeMod")->mValue.getInteger(); rating = iWereWolfFleeMod; } } if (rating != 0.0f) rating += getFightDistanceBias(actor, enemy); return rating; } bool makeFleeDecision(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, float antiFleeRating) { float fleeRating = vanillaRateFlee(actor, enemy); if (fleeRating < 100.0f) fleeRating = 0.0f; if (fleeRating > antiFleeRating) return true; // Run away after summoning a creature if we have nothing to use but fists. if (antiFleeRating == 0.0f && !actor.getClass().getCreatureStats(actor).getSummonedCreatureMap().empty()) return true; return false; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aicombataction.hpp000066400000000000000000000075561503074453300246710ustar00rootroot00000000000000#ifndef OPENMW_AICOMBAT_ACTION_H #define OPENMW_AICOMBAT_ACTION_H #include #include "../mwworld/containerstore.hpp" #include "../mwworld/ptr.hpp" namespace MWMechanics { class Action { public: virtual ~Action() {} virtual void prepare(const MWWorld::Ptr& actor) = 0; 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; } }; class ActionFlee : public Action { public: ActionFlee() {} void prepare(const MWWorld::Ptr& actor) override {} 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; } }; class ActionSpell : public Action { public: 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; ESM::RefId getSpell() const override { return mSpellId; } }; class ActionEnchantedItem : public Action { public: 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; /// Since this action has no animation, apply a small cool down for using it float getActionCooldown() const override { return 0.75f; } }; class ActionPotion : public Action { public: 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; bool isAttackingOrSpell() const override { return false; } /// Since this action has no animation, apply a small cool down for using it float getActionCooldown() const override { return 0.75f; } }; class ActionWeapon : public Action { private: MWWorld::Ptr mAmmunition; MWWorld::Ptr mWeapon; 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) { } /// Equips the given weapon. void prepare(const MWWorld::Ptr& actor) override; float getCombatRange(bool& isRanged) const override; const ESM::Weapon* getWeapon() const override; }; 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 getMaxAttackDistance(const MWWorld::Ptr& actor); bool canFight(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float vanillaRateFlee(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); bool makeFleeDecision(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, float antiFleeRating); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiescort.cpp000066400000000000000000000127671503074453300235200ustar00rootroot00000000000000#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--; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiescort.hpp000066400000000000000000000042151503074453300235120ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIESCORT_H #define GAME_MWMECHANICS_AIESCORT_H #include "typedaipackage.hpp" #include #include namespace ESM { namespace AiSequence { struct AiEscort; } } 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 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); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Escort; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mUseVariableSpeed = true; options.mSideWithTarget = true; return options; } void writeState(ESM::AiSequence::AiSequence& sequence) const override; void fastForward(const MWWorld::Ptr& actor, AiState& state) override; osg::Vec3f getDestination() const override { return osg::Vec3f(mX, mY, mZ); } std::optional getDuration() const override { return mDuration; } 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiface.cpp000066400000000000000000000010521503074453300231000ustar00rootroot00000000000000#include "aiface.hpp" #include "../mwworld/ptr.hpp" #include "steering.hpp" MWMechanics::AiFace::AiFace(float targetX, float targetY) : mTargetX(targetX) , mTargetY(targetY) { } 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)); } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiface.hpp000066400000000000000000000016071503074453300231130ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIFACE_H #define GAME_MWMECHANICS_AIFACE_H #include "typedaipackage.hpp" namespace MWMechanics { /// AiPackage which makes an actor face a certain direction. class AiFace final : public TypedAiPackage { public: AiFace(float targetX, float targetY); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; 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; } private: const float mTargetX; const float mTargetY; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aifollow.cpp000066400000000000000000000226161503074453300235150ustar00rootroot00000000000000#include "aifollow.hpp" #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 "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(); } } namespace MWMechanics { 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; } 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; } 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(); } 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 AiFollow::execute( const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) { const MWWorld::Ptr target = getTarget(); // 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; actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); AiFollowStorage& storage = state.get(); bool& rotate = storage.mTurnActorToTarget; if (rotate) { if (zTurn(actor, storage.mTargetAngleRadians)) rotate = false; return false; } 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) { for (auto& follower : followers) { auto halfExtent = getHalfExtents(follower.second); if (halfExtent > floatingDistance) floatingDistance = halfExtent; } floatingDistance += 128; } floatingDistance += getHalfExtents(target) + 64; floatingDistance += getHalfExtents(actor) * 2; short followDistance = static_cast(floatingDistance); // AiFollow requires the target to be in range and within sight for the initial activation if (!mActive) { storage.mTimer -= duration; if (storage.mTimer < 0) { 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; if (!mAlwaysFollow) // Update if you only follow for a bit { // Check if we've run out of time if (mDuration > 0) { mRemainingDuration -= ((duration * MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale()) / 3600); if (mRemainingDuration <= 0) { mRemainingDuration = mDuration; return true; } } osg::Vec3f finalPos(mX, mY, mZ); if ((actorPos - finalPos).length2() < followDistance * followDistance) // Close-ish to final position { 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; 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 (storage.mMoving) { // 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; } ESM::RefId AiFollow::getFollowedActor() { return mTargetActorRefId; } bool AiFollow::isCommanded() const { return !mOptions.mShouldCancelPreviousAi; } 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; } void AiFollow::fastForward(const MWWorld::Ptr& actor, AiState& state) { // Update duration counter if this package has a duration if (mDuration > 0) mRemainingDuration--; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aifollow.hpp000066400000000000000000000061451503074453300235210ustar00rootroot00000000000000#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::AiSequence { struct AiFollow; } namespace MWMechanics { struct AiFollowStorage : AiTemporaryBase { float mTimer; bool mMoving; float mTargetAngleRadians; bool mTurnActorToTarget; 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 **/ class AiFollow final : public TypedAiPackage { 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aipackage.cpp000066400000000000000000000504431503074453300236050ustar00rootroot00000000000000#include "aipackage.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwphysics/raycasting.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "pathgrid.hpp" #include "steering.hpp" #include namespace { float divOrMax(float dividend, float divisor) { return divisor == 0 ? std::numeric_limits::max() * std::numeric_limits::epsilon() : dividend / divisor; } float getPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) { 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) , 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) { if (mTargetActorRefId.empty()) { mTargetActorId = -2; return MWWorld::Ptr(); } mCachedTarget = MWBase::Environment::get().getWorld()->searchPtr(mTargetActorRefId, false); if (mCachedTarget.isEmpty()) { mTargetActorId = -2; return mCachedTarget; } else mTargetActorId = mCachedTarget.getClass().getCreatureStats(mCachedTarget).getActorId(); } if (mTargetActorId != -1) 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() { // reset all members mReaction.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, 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 MWBase::World* world = MWBase::Environment::get().getWorld(); const DetourNavigator::AgentBounds agentBounds = world->getPathfindingAgentBounds(actor); /// 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) || 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(), agentBounds, position, dest); return false; } mLastDestinationTolerance = destTolerance; const float distToTarget = distance(position, dest); const bool isDestReached = (distToTarget <= destTolerance); const bool actorCanMoveByZ = canActorMoveByZAxis(actor); if (!isDestReached && timerStatus == Misc::TimerStatus::Elapsed) { if (canOpenDoors(actor)) openDoors(actor); const bool wasShortcutting = mIsShortcutting; bool destInLOS = false; // Prohibit shortcuts for AiWander, if the actor can not move in 3 dimensions. mIsShortcutting = actorCanMoveByZ && shortcutPath(position, dest, actor, &destInLOS, actorCanMoveByZ); // try to shortcut first if (!mIsShortcutting) { if (wasShortcutting || doesPathNeedRecalc(dest, actor)) // if need to rebuild path { 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 if (destInLOS && mPathFinder.getPath().size() > 1) { // 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 (distance(position, dest) <= distance(dest, *pPointBeforeDest)) { mPathFinder.clearPath(); mPathFinder.addPointToPath(dest); } } } if (!mPathFinder.getPath().empty()) // Path has points in it { 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 } } } const float pointTolerance = getPointTolerance(actor.getClass().getMaxSpeed(actor), duration, world->getHalfExtents(actor)); const bool smoothMovement = Settings::game().mSmoothMovement; 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 { // turn to destination point zTurn(actor, getZAngleToPoint(position, dest)); smoothTurn(actor, getXAngleToPoint(position, dest), 0); world->removeActorPath(actor); return true; } else if (mPathFinder.getPath().empty()) return false; world->updateActorPath(actor, mPathFinder.getPath(), agentBounds, position, dest); if (mRotateOnTheRunChecks == 0 || 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--; } // turn to next path point by X,Z axes float zAngleToNext = mPathFinder.getZAngleToNext(position.x(), position.y()); zTurn(actor, zAngleToNext); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); const auto destination = getNextPathPoint(dest); mObstacleCheck.update(actor, destination, duration, supportedMovementDirections); if (smoothMovement) { const float smoothTurnReservedDist = 150; auto& movement = actor.getClass().getMovementSettings(actor); float distToNextSqr = osg::Vec2f(destination.x() - position.x(), destination.y() - position.y()).length2(); float diffAngle = zAngleToNext - actor.getRefData().getPosition().rot[2]; if (std::cos(diffAngle) < -0.1) movement.mPosition[0] = movement.mPosition[1] = 0; else if (distToNextSqr > smoothTurnReservedDist * smoothTurnReservedDist) { // Go forward (and slowly turn towards the next path point) movement.mPosition[0] = 0; movement.mPosition[1] = 1; } else { // Next path point is near, so use diagonal movement to follow the path precisely. movement.mPosition[0] = std::sin(diffAngle); movement.mPosition[1] = std::max(std::cos(diffAngle), 0.f); } } // handle obstacles on the way evadeObstacles(actor); return false; } void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor) { // check if stuck due to obstacles if (!mObstacleCheck.isEvading()) return; // first check if obstacle is a door float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); if (!door.isEmpty() && canOpenDoors(actor)) { openDoors(actor); } else { mObstacleCheck.takeEvasiveAction(actor.getClass().getMovementSettings(actor)); } } namespace { bool isDoorOnTheWay(const MWWorld::Ptr& actor, const MWWorld::Ptr& door, const osg::Vec3f& nextPathPoint) { const auto world = MWBase::Environment::get().getWorld(); const auto halfExtents = world->getHalfExtents(actor); const auto position = actor.getRefData().getPosition().asVec3() + osg::Vec3f(0, 0, halfExtents.z()); const auto destination = nextPathPoint + osg::Vec3f(0, 0, halfExtents.z()); return world->hasCollisionWithDoor(door, position, destination); } } void MWMechanics::AiPackage::openDoors(const MWWorld::Ptr& actor) { // note: AiWander currently does not open doors if (getTypeId() == AiPackageTypeId::Wander) return; if (mPathFinder.getPathSize() == 0) return; float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); const MWWorld::Ptr door = getNearbyDoor(actor, distance); if (door == MWWorld::Ptr()) return; if (!door.getCellRef().getTeleport() && door.getClass().getDoorState(door) == MWWorld::DoorState::Idle) { if (!isDoorOnTheWay(actor, door, mPathFinder.getPath().front())) return; if (door.getCellRef().getTrap().empty() && !door.getCellRef().isLocked()) { MWBase::Environment::get().getLuaManager()->objectActivated(door, actor); return; } const ESM::RefId& keyId = door.getCellRef().getKey(); if (keyId.empty()) return; MWWorld::ContainerStore& invStore = actor.getClass().getContainerStore(actor); MWWorld::Ptr keyPtr = invStore.search(keyId); if (!keyPtr.isEmpty()) MWBase::Environment::get().getLuaManager()->objectActivated(door, actor); } } const MWMechanics::PathgridGraph& MWMechanics::AiPackage::getPathGridGraph(const ESM::Pathgrid* pathgrid) const { if (!pathgrid || pathgrid->mPoints.empty()) return PathgridGraph::sEmpty; // static cache is OK for now, pathgrids can never change during runtime static std::map> cache; auto found = cache.find(pathgrid); if (found == cache.end()) 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) { if (!mShortcutProhibited || (mShortcutFailPos - startPoint).length() >= PATHFIND_SHORTCUT_RETRY_DIST) { // check if target is clearly visible isPathClear = !MWBase::Environment::get() .getWorld() ->getRayCasting() ->castRay(startPoint, endPoint, MWPhysics::CollisionType_World | MWPhysics::CollisionType_Door) .mHit; if (destInLOS != nullptr) *destInLOS = isPathClear; if (!isPathClear) return false; // check if an actor can move along the shortcut path isPathClear = checkWayIsClearForActor(startPoint, endPoint, actor); } if (isPathClear) // can shortcut the path { mPathFinder.clearPath(); mPathFinder.addPointToPath(endPoint); return true; } return false; } 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 distToTarget = osg::Vec2f(endPoint.x(), endPoint.y()).length(); const float offsetXY = distToTarget > maxAvoidDist * 1.5 ? maxAvoidDist : maxAvoidDist / 2; // update shortcut prohibit state if (checkWayIsClear(startPoint, endPoint, offsetXY)) { if (mShortcutProhibited) { mShortcutProhibited = false; mShortcutFailPos = osg::Vec3f(); } return true; } else { if (mShortcutFailPos == osg::Vec3f()) { mShortcutProhibited = true; mShortcutFailPos = startPoint; } } return false; } bool MWMechanics::AiPackage::doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const { return mPathFinder.getPath().empty() || getPathDistance(actor, mPathFinder.getPath().back(), newDest) > 10 || mPathFinder.getPathCell() != actor.getCell(); } bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position) { const MWWorld::Cell* playerCell = getPlayer().getCell()->getCell(); if (playerCell->isExterior()) { // get actor's distance from origin of center cell 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()); } else { return false; } } bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const osg::Vec3f& dest) { // get actor's shortest radius for moving in circle float speed = actor.getClass().getMaxSpeed(actor); speed += speed * 0.1f; // 10% real speed inaccuracy float radius = speed / getAngularVelocity(speed); // get radius direction to the center const float* rot = actor.getRefData().getPosition().rot; osg::Quat quatRot(rot[0], -osg::X_AXIS, rot[1], -osg::Y_AXIS, rot[2], -osg::Z_AXIS); osg::Vec3f dir = quatRot * osg::Y_AXIS; // actor's orientation direction is a tangent to circle osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent radiusDir.normalize(); radiusDir *= radius; // pick up the nearest center candidate osg::Vec3f pos = actor.getRefData().getPosition().asVec3(); osg::Vec3f center1 = pos - radiusDir; osg::Vec3f center2 = pos + radiusDir; osg::Vec3f center = (center1 - dest).length2() < (center2 - dest).length2() ? center1 : center2; float distToDest = (center - dest).length(); // if pathpoint is reachable for the actor rotating on the run: // no points of actor's circle should be farther from the center than destination point return (radius <= distToDest); } DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::Ptr& actor) const { const MWWorld::Class& actorClass = actor.getClass(); DetourNavigator::Flags result = DetourNavigator::Flag_none; if ((actorClass.isPureWaterCreature(actor) || (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 | DetourNavigator::Flag_usePathgrid; if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) result |= DetourNavigator::Flag_openDoor; return result; } DetourNavigator::AreaCosts MWMechanics::AiPackage::getAreaCosts( const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const { DetourNavigator::AreaCosts costs; const MWWorld::Class& actorClass = actor.getClass(); const float walkSpeed = [&] { if ((flags & DetourNavigator::Flag_walk) == 0) return 0.0f; if (getTypeId() == AiPackageTypeId::Wander) 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; } osg::Vec3f MWMechanics::AiPackage::getNextPathPoint(const osg::Vec3f& destination) const { return mPathFinder.getPath().empty() ? destination : mPathFinder.getPath().front(); } float MWMechanics::AiPackage::getNextPathPointTolerance( float speed, float duration, const osg::Vec3f& halfExtents) const { if (mPathFinder.getPathSize() <= 1) return std::max(DEFAULT_TOLERANCE, mLastDestinationTolerance); return getPointTolerance(speed, duration, halfExtents); } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aipackage.hpp000066400000000000000000000157531503074453300236170ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIPACKAGE_H #define GAME_MWMECHANICS_AIPACKAGE_H #include #include #include "aipackagetypeid.hpp" #include "aistatefwd.hpp" #include "aitimer.hpp" #include "obstacle.hpp" #include "pathfinding.hpp" #include "../mwworld/ptr.hpp" namespace ESM { struct Cell; namespace AiSequence { struct AiSequence; } } namespace MWMechanics { class CharacterController; class PathgridGraph; /// \brief Base class for AI packages class AiPackage { 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) { 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() { return Options{}; } /// 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; /// 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; } /// Check if package use movement with variable speed bool useVariableSpeed() const { return mOptions.mUseVariableSpeed; } virtual void writeState(ESM::AiSequence::AiSequence& sequence) const {} /// 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; /// 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); } /// 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; } /// 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; } /// 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 std::optional getDistance() const { return std::nullopt; } virtual std::optional getDuration() const { return std::nullopt; } /// Return true if any loaded actor with this AI package must be active. bool alwaysActive() const { return mOptions.mAlwaysActive; } /// Reset pathfinding state void reset(); /// 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); osg::Vec3f getNextPathPoint(const osg::Vec3f& destination) const; float getNextPathPointTolerance(float speed, float duration, const osg::Vec3f& halfExtents) const; 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); /// 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); /// 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); bool doesPathNeedRecalc(const osg::Vec3f& newDest, const MWWorld::Ptr& actor) const; void evadeObstacles(const MWWorld::Ptr& actor); void openDoors(const MWWorld::Ptr& actor); const PathgridGraph& getPathGridGraph(const ESM::Pathgrid* pathgrid) const; DetourNavigator::Flags getNavigatorFlags(const MWWorld::Ptr& actor) const; DetourNavigator::AreaCosts getAreaCosts(const MWWorld::Ptr& actor, DetourNavigator::Flags flags) const; const AiPackageTypeId mTypeId; const Options mOptions; // TODO: all this does not belong here, move into temporary storage PathFinder mPathFinder; ObstacleCheck mObstacleCheck; AiReactionTimer mReaction; ESM::RefId mTargetActorRefId; mutable int mTargetActorId; mutable MWWorld::Ptr mCachedTarget; 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aipackagetypeid.hpp000066400000000000000000000012171503074453300250240ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIPACKAGETYPEID_H #define GAME_MWMECHANICS_AIPACKAGETYPEID_H namespace MWMechanics { /// Enumerates the various AITypes available enum class AiPackageTypeId { None = -1, Wander = 0, Travel = 1, Escort = 2, Follow = 3, Activate = 4, // These 5 are not really handled as Ai Packages in the MW engine // For compatibility do *not* return these in the getCurrentAiPackage script function.. Combat = 5, Pursue = 6, AvoidDoor = 7, Face = 8, Breathe = 9, InternalTravel = 10, Cast = 11 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aipursue.cpp000066400000000000000000000072001503074453300235260ustar00rootroot00000000000000#include "aipursue.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" #include "npcstats.hpp" namespace MWMechanics { AiPursue::AiPursue(const MWWorld::Ptr& actor) { mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); } 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; 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.getCellRef().getCount() || !target.getRefData().isEnabled()) return true; if (isTargetMagicallyHidden(target) && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(target, actor)) return false; if (target.getClass().getCreatureStats(target).isDead()) return true; if (target.getClass().getNpcStats(target).getBounty() <= 0) return true; actor.getClass().getCreatureStats(actor).setDrawState(DrawState::Nothing); // Set the target destination const osg::Vec3f dest = target.getRefData().getPosition().asVec3(); const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); const float pathTolerance = 100.f; // check the true distance in case the target is far away in Z-direction bool reached = pathTo(actor, dest, duration, characterController.getSupportedMovementDirections(), pathTolerance, (actorPos - dest).length(), PathType::Partial) && std::abs(dest.z() - actorPos.z()) < pathTolerance; 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; } actor.getClass().getCreatureStats(actor).setMovementFlag( MWMechanics::CreatureStats::Flag_Run, true); // Make NPC run return false; } 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aipursue.hpp000066400000000000000000000025751503074453300235450ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIPURSUE_H #define GAME_MWMECHANICS_AIPURSUE_H #include "typedaipackage.hpp" namespace ESM { 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 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); AiPursue(const ESM::AiSequence::AiPursue* pursue); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; static constexpr AiPackageTypeId getTypeId() { return AiPackageTypeId::Pursue; } static constexpr Options makeDefaultOptions() { AiPackage::Options options; options.mCanCancel = false; options.mShouldCancelPreviousAi = false; return options; } MWWorld::Ptr getTarget() const override; void writeState(ESM::AiSequence::AiSequence& sequence) const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aisequence.cpp000066400000000000000000000474541503074453300240320ustar00rootroot00000000000000#include "aisequence.hpp" #include #include #include #include #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 "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()); // We need to keep an AiWander storage, if present - it has a state machine. // Not sure about another temporary storages sequence.mAiState.copy(mAiState); mNumCombatPackages = sequence.mNumCombatPackages; mNumPursuitPackages = sequence.mNumPursuitPackages; } AiSequence::AiSequence() : mDone(false) , mLastAiPackage(AiPackageTypeId::None) { } AiSequence::AiSequence(const AiSequence& sequence) { copy(sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; } AiSequence& AiSequence::operator=(const AiSequence& sequence) { if (this != &sequence) { clear(); copy(sequence); mDone = sequence.mDone; mLastAiPackage = sequence.mLastAiPackage; } return *this; } AiSequence::~AiSequence() { clear(); } void AiSequence::onPackageAdded(const AiPackage& package) { if (package.getTypeId() == AiPackageTypeId::Combat) mNumCombatPackages++; else if (package.getTypeId() == AiPackageTypeId::Pursue) mNumPursuitPackages++; assert(mNumCombatPackages >= 0); assert(mNumPursuitPackages >= 0); } void AiSequence::onPackageRemoved(const AiPackage& package) { if (package.getTypeId() == AiPackageTypeId::Combat) { mNumCombatPackages--; if (mNumCombatPackages == 0) mResetFriendlyHits = true; } else if (package.getTypeId() == AiPackageTypeId::Pursue) mNumPursuitPackages--; assert(mNumCombatPackages >= 0); assert(mNumPursuitPackages >= 0); } AiPackageTypeId AiSequence::getTypeId() const { if (mPackages.empty()) return AiPackageTypeId::None; return mPackages.front()->getTypeId(); } bool AiSequence::getCombatTarget(MWWorld::Ptr& targetActor) const { if (getTypeId() != AiPackageTypeId::Combat) return false; targetActor = mPackages.front()->getTarget(); return !targetActor.isEmpty(); } bool AiSequence::getCombatTargets(std::vector& targetActors) const { for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { if ((*it)->getTypeId() == MWMechanics::AiPackageTypeId::Combat) targetActors.push_back((*it)->getTarget()); } return !targetActors.empty(); } AiPackages::iterator AiSequence::erase(AiPackages::iterator package) { // Not sure if manually terminated packages should trigger mDone, probably not? auto& ptr = *package; onPackageRemoved(*ptr); return mPackages.erase(package); } bool AiSequence::isInCombat() const { return mNumCombatPackages > 0; } bool AiSequence::isInPursuit() const { 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) { if ((*it)->getTypeId() == AiPackageTypeId::Combat) { MWWorld::Ptr target2 = (*it)->getTarget(); if (!target2.isEmpty() && target2.getClass().isNpc()) return true; } } return false; } bool AiSequence::hasPackage(AiPackageTypeId typeId) const { auto it = std::find_if(mPackages.begin(), mPackages.end(), [typeId](const auto& package) { return package->getTypeId() == typeId; }); return it != mPackages.end(); } bool AiSequence::isInCombat(const MWWorld::Ptr& actor) const { if (!isInCombat()) return false; for (const auto& package : mPackages) { if (package->getTypeId() == AiPackageTypeId::Combat) { if (package->targetIs(actor)) return true; } } return false; } void AiSequence::removePackagesById(AiPackageTypeId id) { for (auto it = mPackages.begin(); it != mPackages.end();) { if ((*it)->getTypeId() == id) { it = erase(it); } else ++it; } } void AiSequence::stopCombat() { removePackagesById(AiPackageTypeId::Combat); } void AiSequence::stopCombat(const std::vector& targets) { for (auto it = mPackages.begin(); it != mPackages.end();) { if ((*it)->getTypeId() == AiPackageTypeId::Combat && std::find(targets.begin(), targets.end(), (*it)->getTarget()) != targets.end()) { it = erase(it); } else ++it; } } void AiSequence::stopPursuit() { removePackagesById(AiPackageTypeId::Pursue); } bool AiSequence::isPackageDone() const { return mDone; } 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* package = mPackages.front().get(); if (!package->alwaysActive() && outOfRange) return; auto packageTypeId = package->getTypeId(); // workaround ai packages not being handled as in the vanilla engine if (isActualAiPackage(packageTypeId)) mLastAiPackage = packageTypeId; // if active package is combat one, choose nearest target if (packageTypeId == AiPackageTypeId::Combat) { auto itActualCombat = mPackages.end(); float nearestDist = std::numeric_limits::max(); osg::Vec3f vActorPos = actor.getRefData().getPosition().asVec3(); float bestRating = 0.f; for (auto it = mPackages.begin(); it != mPackages.end();) { if ((*it)->getTypeId() != AiPackageTypeId::Combat) break; MWWorld::Ptr target = (*it)->getTarget(); // target disappeared (e.g. summoned creatures) if (target.isEmpty()) { it = erase(it); } else { float rating = 0.f; if (MWMechanics::canFight(actor, target)) rating = MWMechanics::getBestActionRating(actor, target); const ESM::Position& targetPos = target.getRefData().getPosition(); float distTo = (targetPos.asVec3() - vActorPos).length2(); // Small threshold for changing target if (it == mPackages.begin()) distTo = std::max(0.f, distTo - 2500.f); // if a target has higher priority than current target or has same priority but closer if (rating > bestRating || ((distTo < nearestDist) && rating == bestRating)) { nearestDist = distTo; itActualCombat = it; bestRating = rating; } ++it; } } 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 std::rotate(mPackages.begin(), itActualCombat, std::next(itActualCombat)); } package = mPackages.front().get(); packageTypeId = package->getTypeId(); } try { if (package->execute(actor, characterController, mAiState, duration)) { // 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()); } // 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; } else { mDone = false; } } catch (std::exception& e) { Log(Debug::Error) << "Error during AiSequence::execute: " << e.what(); } } 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())) { 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(); } // 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) { dest = getActivePackage().getDestination(actor); } else { dest = actor.getRefData().getPosition().asVec3(); } 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();) { if ((*it)->canCancel()) { it = erase(it); } else ++it; } } // insert new package in correct place depending on priority for (auto it = mPackages.begin(); it != mPackages.end(); ++it) { // 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; } } onPackageAdded(package); mPackages.push_back(package.clone()); // Make sure that temporary storage is empty if (cancelOther) { mAiState.moveIn(std::make_unique()); mAiState.moveIn(std::make_unique()); mAiState.moveIn(std::make_unique()); } } bool MWMechanics::AiSequence::isEmpty() const { return mPackages.empty(); } const AiPackage& MWMechanics::AiSequence::getActivePackage() const { if (mPackages.empty()) throw std::runtime_error(std::string("No AI Package!")); return *mPackages.front(); } void AiSequence::fill(const ESM::AIPackageList& list) { for (const auto& esmPackage : list.mList) { 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)); } } void AiSequence::writeState(ESM::AiSequence::AiSequence& sequence) const { for (const auto& package : mPackages) package->writeState(sequence); sequence.mLastAiPackage = static_cast(mLastAiPackage); } void AiSequence::readState(const ESM::AiSequence::AiSequence& sequence) { if (!sequence.mPackages.empty()) clear(); // Load packages for (auto& container : sequence.mPackages) { 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 (!package.get()) continue; onPackageAdded(*package); mPackages.push_back(std::move(package)); } mLastAiPackage = static_cast(sequence.mLastAiPackage); } void AiSequence::fastForward(const MWWorld::Ptr& actor) { if (!mPackages.empty()) { mPackages.front()->fastForward(actor, mAiState); } } } // namespace MWMechanics openmw-openmw-0.49.0/apps/openmw/mwmechanics/aisequence.hpp000066400000000000000000000134771503074453300240350ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AISEQUENCE_H #define GAME_MWMECHANICS_AISEQUENCE_H #include #include #include #include "aipackagetypeid.hpp" #include "aistate.hpp" #include namespace MWWorld { class Ptr; } namespace ESM { namespace AiSequence { struct AiSequence; } } namespace MWMechanics { class AiPackage; class CharacterController; 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 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); AiPackages::iterator erase(AiPackages::iterator package); public: /// Default constructor AiSequence(); /// Copy Constructor AiSequence(const AiSequence& sequence); /// Assignment operator AiSequence& operator=(const AiSequence& sequence); virtual ~AiSequence(); /// 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(); } /// 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()); } /// 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); } /// Returns currently executing AiPackage type /** \see enum class AiPackageTypeId **/ AiPackageTypeId getTypeId() 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 target if combat package is currently active, return false otherwise bool getCombatTarget(MWWorld::Ptr& targetActor) 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 combat package? bool isInCombat() const; /// Is there any pursuit package. bool isInPursuit() const; /// Is the actor fleeing? bool isFleeing() const; /// Removes all packages using the specified id. void removePackagesById(AiPackageTypeId id); /// Are we in combat with any other actor, who's also engaging us? bool isEngagedWithActor() const; /// Does this AI sequence have the given package type? bool hasPackage(AiPackageTypeId typeId) const; /// 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 with the given targets void stopCombat(const std::vector& targets); /// Has a package been completed during the last update? bool isPackageDone() const; /// Removes all pursue packages until first non-pursue or stack empty. void stopPursuit(); /// Execute current package, switching if needed. void execute(const MWWorld::Ptr& actor, CharacterController& characterController, float duration, bool outOfRange = false); /// Simulate the passing of time using the currently active AI package void fastForward(const MWWorld::Ptr& actor); /// Remove all packages. void clear(); ///< 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); /// Return the current active package. /** If there is no active package, it will throw an exception **/ const AiPackage& getActivePackage() 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aisetting.hpp000066400000000000000000000003411503074453300236640ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_AISETTING_H #define OPENMW_MWMECHANICS_AISETTING_H namespace MWMechanics { enum class AiSetting { Hello = 0, Fight = 1, Flee = 2, Alarm = 3 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aistate.hpp000066400000000000000000000036401503074453300233340ustar00rootroot00000000000000#ifndef AISTATE_H #define AISTATE_H #include "aistatefwd.hpp" #include "aitemporarybase.hpp" #include namespace MWMechanics { /** \brief stores one object of any class derived from Base. * Requesting a certain derived class via get() either returns * the stored object if it has the correct type or otherwise replaces * it with an object of the requested type. */ template class DerivedClassStorage { private: std::unique_ptr mStorage; public: /// \brief returns reference to stored object or deletes it and creates a fitting template Derived& get() { Derived* result = dynamic_cast(mStorage.get()); if (result == nullptr) { auto storage = std::make_unique(); result = storage.get(); mStorage = std::move(storage); } // return a reference to the (new allocated) object return *result; } /// \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.get()); if (result != nullptr) destination.store(*result); } template void store(const Derived& payload) { mStorage = std::make_unique(payload); } /// \brief takes ownership of the passed object template void moveIn(std::unique_ptr&& storage) { mStorage = std::move(storage); } }; } #endif // AISTATE_H openmw-openmw-0.49.0/apps/openmw/mwmechanics/aistatefwd.hpp000066400000000000000000000004641503074453300240360ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aitemporarybase.hpp000066400000000000000000000010771503074453300250730ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aitimer.hpp000066400000000000000000000014241503074453300233320ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_AITIMER_H #define OPENMW_MECHANICS_AITIMER_H #include #include namespace MWMechanics { constexpr float AI_REACTION_TIME = 0.25f; class AiReactionTimer { public: static constexpr float sDeviation = 0.1f; AiReactionTimer(Misc::Rng::Generator& prng) : mPrng{ prng } , mImpl{ AI_REACTION_TIME, sDeviation, Misc::Rng::deviate(0, sDeviation, prng) } { } Misc::TimerStatus update(float duration) { return mImpl.update(duration, mPrng); } void reset() { mImpl.reset(Misc::Rng::deviate(0, sDeviation, mPrng)); } private: Misc::Rng::Generator& mPrng; Misc::DeviatingPeriodicTimer mImpl; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/aitravel.cpp000066400000000000000000000132701503074453300235040ustar00rootroot00000000000000#include "aitravel.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "character.hpp" #include "creaturestats.hpp" #include "movement.hpp" namespace { 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, 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) , mDestinationTimer(TRAVEL_FINISH_TIME) { } AiTravel::AiTravel(float x, float y, float z, bool repeat) : AiTravel(x, y, z, repeat, this) { } 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) { 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)) 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); // Note: we should cancel internal "return after combat" package, if original location is too far away if (!isWithinMaxRange(targetPos, actorPos)) return mHidden; if (pathTo(actor, targetPos, duration, characterController.getSupportedMovementDirections())) { actor.getClass().getMovementSettings(actor).mPosition[1] = 0; return true; } // 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) { mDestinationTimer = TRAVEL_FINISH_TIME; 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) { 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, pos); actor.getClass().adjustPosition(actor, false); reset(); } void AiTravel::writeState(ESM::AiSequence::AiSequence& sequence) const { 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 = std::move(travel); sequence.mPackages.push_back(std::move(package)); } AiInternalTravel::AiInternalTravel(float x, float y, float z) : AiTravel(x, y, z, this) { } AiInternalTravel::AiInternalTravel(const ESM::AiSequence::AiTravel* travel) : AiTravel(travel->mData.mX, travel->mData.mY, travel->mData.mZ, this) { } std::unique_ptr AiInternalTravel::clone() const { return std::make_unique(*this); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aitravel.hpp000066400000000000000000000036041503074453300235110ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiwander.cpp000066400000000000000000001207721503074453300234750ustar00rootroot00000000000000#include "aiwander.hpp" #include #include #include #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 "../mwworld/esmstore.hpp" #include "../mwphysics/raycasting.hpp" #include "actorutil.hpp" #include "character.hpp" #include "creaturestats.hpp" #include "movement.hpp" #include "pathgrid.hpp" namespace MWMechanics { static const int COUNT_BEFORE_RESET = 10; static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; // to prevent overcrowding static const int DESTINATION_TOLERANCE = 64; // distance must be long enough that NPC will need to move to get there. static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2; static const std::size_t MAX_IDLE_SIZE = 8; const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", "idle8", "idle9", }; namespace { inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) { if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) return 1; return COUNT_BEFORE_RESET; } osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) { 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) { 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()->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() ->getRayCasting() ->castRay(position, visibleDestination, { actor }, {}, mask) .mHit; } void stopMovement(const MWWorld::Ptr& actor) { auto& movementSettings = actor.getClass().getMovementSettings(actor); movementSettings.mPosition[0] = 0; movementSettings.mPosition[1] = 0; } std::vector getInitialIdle(const std::vector& idle) { std::vector result(MAX_IDLE_SIZE, 0); std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); return result; } std::vector getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) { return std::vector(std::begin(idle), std::end(idle)); } } 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) { } /* * AiWander high level states (0.29.0). Not entirely accurate in some cases * e.g. non-NPC actors do not greet and some creatures may be moving even in * the IdleNow state. * * [select node, * build path] * +---------->MoveNow----------->Walking * | | * [allowed | | * nodes] | [hello if near] | * start--->ChooseAction----->IdleNow | * ^ ^ | | * | | | | * | +-----------+ | * | | * +----------------------------------+ * * * New high level states. Not exactly as per vanilla (e.g. door stuff) * but the differences are required because our physics does not work like * vanilla and therefore have to compensate/work around. * * [select node, [if stuck evade * build path] or remove nodes if near door] * +---------->MoveNow<---------->Walking * | ^ | | * | |(near door) | | * [allowed | | | | * nodes] | [hello if near] | | * start--->ChooseAction----->IdleNow | | * ^ ^ | ^ | | * | | | | (stuck near | | * | +-----------+ +---------------+ | * | player) | * +----------------------------------+ * * NOTE: non-time critical operations are run once every 250ms or so. * * TODO: It would be great if door opening/closing can be detected and pathgrid * links dynamically updated. Currently (0.29.0) AiWander allows choosing a * destination beyond closed doors which sometimes makes the actors stuck at the * door and impossible for the player to open the door. * * For now detect being stuck at the door and simply delete the nodes from the * allowed set. The issue is when the door opens the allowed set is not * re-calculated. However this would not be an issue in most cases since hostile * 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) { MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if (cStats.isDead() || cStats.getHealth().getCurrent() <= 0) return true; // Don't bother with dead actors // get or create temporary storage AiWanderStorage& storage = state.get(); mRemainingDuration -= ((duration * MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale()) / 3600); cStats.setDrawState(DrawState::Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); // If there is already a destination due to the package having been interrupted by a combat or pursue package, // 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(pathgrid)); } else { const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); constexpr float endTolerance = 0; const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); } if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); } if (!cStats.getMovementFlag(CreatureStats::Flag_ForceJump) && !cStats.getMovementFlag(CreatureStats::Flag_ForceSneak)) { GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); if (greetingState == Greet_InProgress) { if (storage.mState == AiWanderStorage::Wander_Walking) { stopMovement(actor); mObstacleCheck.clear(); storage.setState(AiWanderStorage::Wander_IdleNow); } } } doPerFrameActionsForState(actor, duration, characterController.getSupportedMovementDirections(), storage); if (storage.mReaction.update(duration) == Misc::TimerStatus::Waiting) return false; return reactionTimeActions(actor, storage, pos); } bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) { if (mDistance <= 0) storage.mCanWanderAlongPathGrid = false; if (isPackageCompleted()) { stopWalking(actor); // Reset package so it can be used again mRemainingDuration = mDuration; return true; } if (!mStoredInitialActorPosition) { mInitialActorPosition = actor.getRefData().getPosition().asVec3(); mStoredInitialActorPosition = true; } // Initialization to discover & store allowed node points for this actor. if (storage.mPopulateAvailableNodes) { getAllowedNodes(actor, storage); } 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, prng) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { wanderNearStart(actor, storage, mDistance); } storage.mCanWanderAlongPathGrid = false; } // 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) { // Typically want to idle for a short time before the next wander if (Misc::Rng::rollDice(100, prng) >= 96) { wanderNearStart(actor, storage, mDistance); } else { storage.setState(AiWanderStorage::Wander_IdleNow); } } else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; } // If Wandering manually and hit an obstacle, stop 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 (!storage.mAllowedNodes.empty()) { setPathToAnAllowedNode(actor, storage, pos); } } } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted()) { completeManualWalking(actor, storage); } if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) || isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) completeManualWalking(actor, storage); return false; // AiWander package not yet completed } osg::Vec3f AiWander::getDestination(const MWWorld::Ptr& actor) const { if (mHasDestination) return mDestination; return actor.getRefData().getPosition().asVec3(); } bool AiWander::isPackageCompleted() const { // End package if duration is complete return mDuration && mRemainingDuration <= 0; } /* * Commands actor to walk to a random location near original spawn location. */ 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 agentBounds = world->getPathfindingAgentBounds(actor); const auto navigator = world->getNavigator(); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); do { // Determine a random location within radius of original position 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 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); // Check if land creature will walk onto water or if water creature will swim onto land if (!isWaterCreature && destinationIsAtWater(actor, mDestination)) continue; if (isDestinationHidden(actor, mDestination)) continue; if (isAreaOccupiedByOtherActor(actor, mDestination)) continue; constexpr float endTolerance = 0; if (isWaterCreature || isFlyingCreature) mPathFinder.buildStraightPath(mDestination); else mPathFinder.buildPathByNavMesh(actor, currentPosition, mDestination, agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); if (mPathFinder.isPathConstructed()) { storage.setState(AiWanderStorage::Wander_Walking, true); mHasDestination = true; mUsePathgrid = false; } break; } while (--attempts); } /* * 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); 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) { stopWalking(actor); mObstacleCheck.clear(); storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage) { // 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) { 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_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); return; } } } void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. storage.mCheckIdlePositionTimer += duration; if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) { storage.mCheckIdlePositionTimer = 0; // restart timer static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); storage.mTrimCurrentNode = false; // just in case return; } } // Check if idle animation finished GreetingState greetingState = MWBase::Environment::get().getMechanicsManager()->getGreetingState(actor); if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) { if (mPathFinder.isPathConstructed()) storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); else storage.setState(AiWanderStorage::Wander_ChooseAction); } } bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const { const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) { osg::Vec3f point(node.mX, node.mY, node.mZ); if ((actorPos - point).length2() < distance * distance) return true; } return false; } 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, supportedMovementDirections, DESTINATION_TOLERANCE)) { stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); } else { // have not yet reached the destination evadeObstacles(actor, storage); } } void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // Wait while fully stop before starting idle animation (important if "smooth movement" is enabled). if (actor.getClass().getCurrentSpeed(actor) > 0) return; unsigned short idleAnimation = getRandomIdle(); storage.mIdleAnimation = idleAnimation; if (!idleAnimation && mDistance) { storage.setState(AiWanderStorage::Wander_MoveNow); return; } if (idleAnimation) { if (std::find(storage.mBadIdles.begin(), storage.mBadIdles.end(), idleAnimation) == storage.mBadIdles.end()) { if (!playIdle(actor, idleAnimation)) { storage.mBadIdles.push_back(idleAnimation); storage.setState(AiWanderStorage::Wander_ChooseAction); return; } } } storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage) { if (mObstacleCheck.isEvading()) { // first check if we're walking into a door static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); if (proximityToDoor(actor, distance)) { // remove allowed points then select another random destination storage.mTrimCurrentNode = true; trimAllowedNodes(storage.mAllowedNodes, mPathFinder); mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_MoveNow); } storage.mStuckCount++; // TODO: maybe no longer needed } // if stuck for sufficiently long, act like current location was the destination if (storage.mStuckCount >= getCountBeforeReset(actor)) // something has gone wrong, reset { mObstacleCheck.clear(); stopWalking(actor); storage.setState(AiWanderStorage::Wander_ChooseAction); storage.mStuckCount = 0; } } void AiWander::setPathToAnAllowedNode( const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) { auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; const osg::Vec3f start = actorPos.asVec3(); // don't take shortcuts for wandering const ESM::Pathgrid* pathgrid = world->getStore().get().search(*actor.getCell()->getCell()); const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid)); 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): ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); // check if mCurrentNode was taken out of mAllowedNodes if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) storage.mTrimCurrentNode = false; else storage.mAllowedNodes.push_back(storage.mCurrentNode); storage.mCurrentNode = temp; storage.setState(AiWanderStorage::Wander_Walking); } // Choose a different node and delete this one from possible nodes because it is uncreachable: else storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); } void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) // Every now and then check whether one of the doors is opened. (maybe // at the end of playing idle?) If the door is opened then re-calculate // allowed nodes starting from the spawn point. auto paths = pathfinder.getPath(); while (paths.size() >= 2) { const auto pt = paths.back(); for (unsigned int j = 0; j < nodes.size(); j++) { // FIXME: doesn't handle a door with the same X/Y // coordinates but with a different Z if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5) { nodes.erase(nodes.begin() + j); break; } } paths.pop_back(); } } void AiWander::stopWalking(const MWWorld::Ptr& actor) { mPathFinder.clearPath(); mHasDestination = false; stopMovement(actor); } bool AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, groupName, 0, 1); } else { Log(Debug::Verbose) << "Attempted to play out of range idle animation \"" << idleSelect << "\" for " << actor.getCellRef().getRefId(); return false; } } bool AiWander::checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if ((GroupIndex_MinIdle <= idleSelect) && (idleSelect <= GroupIndex_MaxIdle)) { const std::string_view groupName = sIdleSelectToGroupName[idleSelect - GroupIndex_MinIdle]; return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, groupName); } else { return false; } } int AiWander::getRandomIdle() const { 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) { newIdle = GroupIndex_MinIdle + i; maxRoll = roll; } } return newIdle; } void AiWander::fastForward(const MWWorld::Ptr& actor, AiState& state) { // Update duration counter mRemainingDuration--; if (mDistance == 0) return; AiWanderStorage& storage = state.get(); if (storage.mPopulateAvailableNodes) getAllowedNodes(actor, storage); if (storage.mAllowedNodes.empty()) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; 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); // add offset only if the selected pathgrid is occupied by another actor if (isPathGridOccupied) { ESM::Pathgrid::PointList points; getNeighbouringNodes(dest, actor.getCell(), points); // there are no neighbouring nodes, nowhere to move if (points.empty()) return; bool isOccupied = false; // AI will try to move the NPC towards every neighboring node until suitable place will be found while (!points.empty()) { 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); float length = dir.length(); dir.normalize(); for (int j = 1; j <= 3; j++) { // move for 5-15% towards random neighboring node dest = PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f)); worldDest = converter.toWorldPoint(dest); isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( PathFinder::makeOsgVec3(worldDest), 60); if (!isOccupied) break; } if (!isOccupied) break; // Will try an another neighboring node points.erase(points.begin() + randomIndex); } // there is no free space, nowhere to move if (isOccupied) 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. dest.mZ += 60; converter.toWorld(dest); state.moveIn(std::make_unique()); 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) { 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(pathgrid).getNeighbouringPoints(index, points); } void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) { // infrequently used, therefore no benefit in caching it as a member const MWWorld::CellStore* cellStore = actor.getCell(); const ESM::Pathgrid* pathgrid = MWBase::Environment::get().getESMStore()->get().search(*cellStore->getCell()); storage.mAllowedNodes.clear(); // If there is no path this actor doesn't go anywhere. See: // 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)) storage.mCanWanderAlongPathGrid = false; // A distance value passed into the constructor indicates how far the // actor can wander from the spawn position. AiWander assumes that // pathgrid points are available, and uses them to randomly select wander // destinations within the allowed set of pathgrid points (nodes). // ... pathgrids don't usually include water, so swimmers ignore them if (mDistance && storage.mCanWanderAlongPathGrid && !actor.getClass().isPureWaterCreature(actor)) { // get NPC's position in local (i.e. cell) coordinates 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 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(pathgrid).isPointConnected(closestPointIndex, counter)) { storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); pointIndex = counter; } } if (storage.mAllowedNodes.size() == 1) { storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); } if (!storage.mAllowedNodes.empty()) { setCurrentNodeToClosestAllowedNode(storage); } } storage.mPopulateAvailableNodes = false; } // When only one path grid point in wander distance, // 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(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, const Misc::CoordinateConverter& converter) { for (const auto& edge : pathGrid->mEdges) { if (edge.mV0 == pointIndex) { 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) { osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; float length = delta.length(); delta.normalize(); int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); // must not travel longer than distance between waypoints or NPC goes past waypoint distance = std::min(distance, static_cast(length)); delta *= distance; storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); } void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) { float distanceToClosestNode = std::numeric_limits::max(); size_t index = 0; for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) { osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); float tempDist = (mInitialActorPosition - nodePos).length2(); if (tempDist < distanceToClosestNode) { index = i; distanceToClosestNode = tempDist; } } storage.mCurrentNode = storage.mAllowedNodes[index]; storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); } void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const { float remainingDuration; if (mRemainingDuration > 0 && mRemainingDuration < 24) remainingDuration = mRemainingDuration; else remainingDuration = mDuration; 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) wander->mData.mIdle[i] = mIdle[i]; wander->mData.mShouldRepeat = mOptions.mRepeat; wander->mStoredInitialActorPosition = mStoredInitialActorPosition; if (mStoredInitialActorPosition) wander->mInitialActorPosition = mInitialActorPosition; ESM::AiSequence::AiPackageContainer package; package.mType = ESM::AiSequence::Ai_Wander; package.mPackage = std::move(wander); sequence.mPackages.push_back(std::move(package)); } 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)) , mRemainingDuration(wander->mDurationData.mRemainingDuration) , mTimeOfDay(wander->mData.mTimeOfDay) , mIdle(getInitialIdle(wander->mData.mIdle)) , mStoredInitialActorPosition(wander->mStoredInitialActorPosition) , mHasDestination(false) , mDestination(osg::Vec3f(0, 0, 0)) , mUsePathgrid(false) { if (mStoredInitialActorPosition) mInitialActorPosition = wander->mInitialActorPosition; if (mRemainingDuration <= 0 || mRemainingDuration >= 24) mRemainingDuration = mDuration; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/aiwander.hpp000066400000000000000000000154531503074453300235010ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H #include "typedaipackage.hpp" #include #include #include "aitemporarybase.hpp" #include "aitimer.hpp" #include "pathfinding.hpp" namespace ESM { struct Cell; namespace AiSequence { struct AiWander; } } 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. struct AiWanderStorage : AiTemporaryBase { AiReactionTimer mReaction; // AiWander states enum WanderState { Wander_ChooseAction, Wander_IdleNow, Wander_MoveNow, Wander_Walking }; WanderState mState; bool mIsWanderingManually; bool mCanWanderAlongPathGrid; unsigned short mIdleAnimation; std::vector mBadIdles; // Idle animations that when called cause errors // do we need to calculate allowed nodes based on mDistance bool mPopulateAvailableNodes; // allowed pathgrid nodes based on mDistance from the spawn point std::vector mAllowedNodes; ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; float mCheckIdlePositionTimer; int mStuckCount; AiWanderStorage(); void setState(const WanderState wanderState, const bool isManualWander = false) { mState = wanderState; mIsWanderingManually = isManualWander; } }; /// \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); explicit AiWander(const ESM::AiSequence::AiWander* wander); bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) override; 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; osg::Vec3f getDestination(const MWWorld::Ptr& actor) const override; osg::Vec3f getDestination() const override { if (!mHasDestination) return osg::Vec3f(0, 0, 0); return mDestination; } bool isStationary() const { return mDistance == 0; } std::optional getDistance() const override { return mDistance; } std::optional getDuration() const override { return static_cast(mDuration); } const std::vector& getIdle() const { return mIdle; } static std::string_view getIdleGroupName(size_t index) { return sIdleSelectToGroupName[index]; } private: void stopWalking(const MWWorld::Ptr& actor); /// 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; 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 mStoredInitialActorPosition; osg::Vec3f mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell bool mHasDestination; osg::Vec3f mDestination; bool mUsePathgrid; void getNeighbouringNodes( ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); void trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder); // constants for converting idleSelect values into groupNames enum GroupIndex { GroupIndex_MinIdle = 2, GroupIndex_MaxIdle = 9 }; void setCurrentNodeToClosestAllowedNode(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); /// lookup table for converting idleSelect value to groupName static const std::string_view sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1]; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/alchemy.cpp000066400000000000000000000461761503074453300233320ustar00rootroot00000000000000#include "alchemy.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.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) { } std::vector MWMechanics::Alchemy::listEffects() const { // 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 (mIngredients[slotI].isEmpty()) continue; const ESM::Ingredient* ingredient = mIngredients[slotI].get()->mBase; for (size_t slotJ = slotI + 1; slotJ < mIngredients.size(); ++slotJ) { 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)) { if (std::find(effects.begin(), effects.end(), *key) != effects.end()) continue; if (containsEffect(*ingredient2, *key)) effects.push_back(*key); } } } } return effects; } void MWMechanics::Alchemy::applyTools(int flags, float& value) const { bool magnitude = !(flags & ESM::MagicEffect::NoMagnitude); bool duration = !(flags & ESM::MagicEffect::NoDuration); bool negative = (flags & ESM::MagicEffect::Harmful) != 0; int tool = negative ? ESM::Apparatus::Alembic : ESM::Apparatus::Retort; int setup = 0; if (!mTools[tool].isEmpty() && !mTools[ESM::Apparatus::Calcinator].isEmpty()) setup = 1; else if (!mTools[tool].isEmpty()) setup = 2; else if (!mTools[ESM::Apparatus::Calcinator].isEmpty()) setup = 3; 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 quality = 1; switch (setup) { case 1: 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); break; case 3: quality = magnitude && duration ? calcinatorQuality : calcinatorQuality + 0.5f; break; } if (setup == 3 || !negative) { value += quality; } else { if (quality == 0) throw std::runtime_error("invalid derived alchemy apparatus quality"); value /= quality; } } void MWMechanics::Alchemy::updateEffects() { mEffects.clear(); mValue = 0; if (countIngredients() < 2 || mAlchemist.isEmpty() || mTools[ESM::Apparatus::MortarPestle].isEmpty()) return; // find effects std::vector effects = listEffects(); // general alchemy factor float x = getAlchemyFactor(); x *= mTools[ESM::Apparatus::MortarPestle].get()->mBase->mData.mQuality; x *= MWBase::Environment::get() .getESMStore() ->get() .find("fPotionStrengthMult") ->mValue.getFloat(); // value mValue = static_cast( x * MWBase::Environment::get().getESMStore()->get().find("iAlchemyMod")->mValue.getFloat()); // build quantified effect list for (const auto& effectKey : effects) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(effectKey.mId); if (magicEffect->mData.mBaseCost <= 0) { 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() .getESMStore() ->get() .find("fPotionT1MagMult") ->mValue.getFloat(); if (fPotionT1MagMul <= 0) throw std::runtime_error("invalid gmst: fPotionT1MagMul"); float fPotionT1DurMult = MWBase::Environment::get() .getESMStore() ->get() .find("fPotionT1DurMult") ->mValue.getFloat(); 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; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)) applyTools(magicEffect->mData.mFlags, magnitude); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) applyTools(magicEffect->mData.mFlags, duration); duration = roundf(duration); magnitude = roundf(magnitude); if (magnitude > 0 && duration > 0) { ESM::ENAMstruct effect; effect.mEffectID = effectKey.mId; effect.mAttribute = -1; effect.mSkill = -1; if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill) effect.mSkill = ESM::Skill::refIdToIndex(effectKey.mArg); else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute) effect.mAttribute = ESM::Attribute::refIdToIndex(effectKey.mArg); effect.mRange = 0; effect.mArea = 0; effect.mDuration = static_cast(duration); effect.mMagnMin = effect.mMagnMax = static_cast(magnitude); mEffects.push_back(effect); } } } const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const { const MWWorld::Store& potions = MWBase::Environment::get().getESMStore()->get(); MWWorld::Store::iterator iter = potions.begin(); for (; iter != potions.end(); ++iter) { 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.mFlags != toFind.mData.mFlags) continue; // Don't choose an ID that came from the content files, would have unintended side effects // where alchemy can be used to produce quest-relevant items if (!potions.isDynamic(iter->mId)) continue; bool mismatch = false; for (size_t i = 0; i < iter->mEffects.mList.size(); ++i) { const ESM::IndexedENAMstruct& first = iter->mEffects.mList[i]; const ESM::ENAMstruct& second = mEffects[i]; 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; } } if (!mismatch) return &(*iter); } return nullptr; } void MWMechanics::Alchemy::removeIngredients() { for (TIngredientsContainer::iterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) if (!iter->isEmpty()) { iter->getContainerStore()->remove(*iter, 1); if (iter->getCellRef().getCount() < 1) *iter = MWWorld::Ptr(); } updateEffects(); } void MWMechanics::Alchemy::addPotion(const std::string& name) { ESM::Potion newRecord; newRecord.mData.mWeight = 0; for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) newRecord.mData.mWeight += iter->get()->mBase->mData.mWeight; if (countIngredients() > 0) newRecord.mData.mWeight /= countIngredients(); newRecord.mData.mValue = mValue; newRecord.mData.mFlags = 0; newRecord.mRecordFlags = 0; newRecord.mName = name; 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" }; newRecord.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif"; newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds"; newRecord.mEffects.populate(mEffects); const ESM::Potion* record = getRecord(newRecord); if (!record) record = MWBase::Environment::get().getESMStore()->insert(newRecord); mAlchemist.getClass().getContainerStore(mAlchemist).add(record->mId, 1); } void MWMechanics::Alchemy::increaseSkill() { mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, ESM::Skill::Alchemy_CreatePotion); } float MWMechanics::Alchemy::getAlchemyFactor() const { 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()); } int MWMechanics::Alchemy::countIngredients() const { int ingredients = 0; for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) ++ingredients; return ingredients; } int MWMechanics::Alchemy::countPotionsToBrew() const { Result readyStatus = getReadyStatus(); if (readyStatus != Result_Success) return 0; int toBrew = -1; for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter) if (!iter->isEmpty()) { int count = iter->getCellRef().getCount(); if ((count > 0 && count < toBrew) || toBrew < 0) toBrew = count; } return toBrew; } void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc) { mAlchemist = npc; mIngredients.resize(4); std::fill(mIngredients.begin(), mIngredients.end(), MWWorld::Ptr()); mTools.resize(4); std::vector prevTools(mTools); std::fill(mTools.begin(), mTools.end(), MWWorld::Ptr()); mEffects.clear(); MWWorld::ContainerStore& store = npc.getClass().getContainerStore(npc); 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 (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) continue; mTools[type] = *iter; } } MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::beginTools() const { return mTools.begin(); } MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::endTools() const { return mTools.end(); } MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::beginIngredients() const { return mIngredients.begin(); } MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients() const { return mIngredients.end(); } void MWMechanics::Alchemy::clear() { mAlchemist = MWWorld::Ptr(); mIngredients.clear(); mEffects.clear(); setPotionName(""); } void MWMechanics::Alchemy::setPotionName(const std::string& name) { mPotionName = name; } int MWMechanics::Alchemy::addIngredient(const MWWorld::Ptr& ingredient) { // find a free slot int slot = -1; for (int i = 0; i < static_cast(mIngredients.size()); ++i) if (mIngredients[i].isEmpty()) { slot = i; break; } if (slot == -1) return -1; for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter) if (!iter->isEmpty() && ingredient.getCellRef().getRefId() == iter->getCellRef().getRefId()) return -1; mIngredients[slot] = ingredient; updateEffects(); return slot; } void MWMechanics::Alchemy::removeIngredient(size_t index) { 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(); } MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const { return mEffects.end(); } 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().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); } MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const { if (mTools[ESM::Apparatus::MortarPestle].isEmpty()) return Result_NoMortarAndPestle; if (countIngredients() < 2) return Result_LessThanTwoIngredients; if (mPotionName.empty()) return Result_NoName; if (listEffects().empty()) return Result_NoEffects; return Result_Success; } MWMechanics::Alchemy::Result MWMechanics::Alchemy::create(const std::string& name, int& count) { setPotionName(name); Result readyStatus = getReadyStatus(); if (readyStatus == Result_NoEffects) removeIngredients(); 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) { if (createSingle() == Result_Success) { result = Result_Success; brewedCount++; } } count = brewedCount; return result; } MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle() { if (beginEffects() == endEffects()) { // all effects were nullified due to insufficient skill removeIngredients(); return Result_RandomFailure; } auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (getAlchemyFactor() < Misc::Rng::roll0to99(prng)) { removeIngredients(); return Result_RandomFailure; } addPotion(mPotionName); removeIngredients(); increaseSkill(); return Result_Success; } std::string MWMechanics::Alchemy::suggestPotionName() { std::vector effects = listEffects(); if (effects.empty()) return {}; return effects.begin()->toString(); } std::vector MWMechanics::Alchemy::effectsDescription(const MWWorld::ConstPtr& ptr, const int alchemySkill) { std::vector effects; const auto& item = ptr.get()->mBase; 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 (size_t i = 0; i < sNumEffects; ++i) { const auto effectID = data.mEffectID[i]; if (alchemySkill < fWortChanceValue * static_cast(i + 1)) break; if (effectID != -1) { 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; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/alchemy.hpp000066400000000000000000000111431503074453300233210ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_ALCHEMY_H #define GAME_MWMECHANICS_ALCHEMY_H #include #include #include "../mwworld/ptr.hpp" namespace ESM { struct Potion; } namespace MWMechanics { struct EffectKey; /// \brief Potion creation via alchemy skill class Alchemy { public: Alchemy(); typedef std::vector TToolsContainer; typedef TToolsContainer::const_iterator TToolsIterator; typedef std::vector TIngredientsContainer; typedef TIngredientsContainer::const_iterator TIngredientsIterator; typedef std::vector TEffectsContainer; typedef TEffectsContainer::const_iterator TEffectsIterator; enum Result { Result_Success, 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; void applyTools(int flags, float& value) const; void updateEffects(); Result getReadyStatus() const; 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 removeIngredients(); ///< Remove selected ingredients from alchemist's inventory, cleanup selected ingredients and /// update effect list accordingly. void addPotion(const std::string& name); ///< Add a potion to the alchemist's inventory. void increaseSkill(); ///< Increase alchemist's skill. 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. float getAlchemyFactor() const; int countIngredients() const; TEffectsIterator beginEffects() const; TEffectsIterator endEffects() const; public: int countPotionsToBrew() const; ///< calculates maximum amount of potions, which you can make from selected ingredients static bool knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr& npc); ///< Does npc have sufficient alchemy skill to know about this potion effect? 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 beginTools() const; ///< \attention Iterates over tool slots, not over tools. Some of the slots may be empty. TToolsIterator endTools() const; TIngredientsIterator beginIngredients() const; ///< \attention Iterates over ingredient slots, not over ingredients. Some of the slots may be empty. TIngredientsIterator endIngredients() const; void clear(); ///< Remove alchemist, tools and ingredients. void setPotionName(const std::string& name); ///< Set name of potion to create std::vector listEffects() const; ///< List all effects shared by at least two ingredients. 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 addApparatus(const MWWorld::Ptr& apparatus); ///< Add apparatus into the appropriate slot. void removeIngredient(size_t 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 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/attacktype.hpp000066400000000000000000000003451503074453300240520ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_ATTACKTYPE_H #define OPENMW_MWMECHANICS_ATTACKTYPE_H namespace MWMechanics { enum class AttackType { NoAttack, Any, Chop, Slash, Thrust }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/autocalcspell.cpp000066400000000000000000000326321503074453300245330ustar00rootroot00000000000000#include "autocalcspell.hpp" #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "spellutil.hpp" namespace MWMechanics { struct SchoolCaps { int mCount; int mLimit; bool mReachedLimit; int mMinCost; ESM::RefId mWeakestSpell; }; std::vector autoCalcNpcSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race) { const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->mValue.getFloat(); float baseMagicka = fNPCbaseMagickaMult * actorAttributes.at(ESM::Attribute::Intelligence).getBase(); std::map schoolCaps; for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) { if (!skill.mSchool) continue; SchoolCaps caps; caps.mCount = 0; caps.mLimit = skill.mSchool->mAutoCalcMax; caps.mReachedLimit = skill.mSchool->mAutoCalcMax <= 0; caps.mMinCost = std::numeric_limits::max(); caps.mWeakestSpell = ESM::RefId(); schoolCaps[skill.mId] = caps; } std::vector selectedSpells; 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. for (const ESM::Spell& spell : spells) { if (spell.mData.mType != ESM::Spell::ST_Spell) continue; if (!(spell.mData.mFlags & ESM::Spell::F_Autocalc)) continue; static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->mValue.getInteger(); int spellCost = MWMechanics::calcSpellCost(spell); if (baseMagicka < iAutoSpellTimesCanCast * spellCost) continue; if (race && race->mPowers.exists(spell.mId)) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; ESM::RefId school; float skillTerm; calcWeakestSchool(&spell, actorSkills, school, skillTerm); if (school.empty()) continue; SchoolCaps& cap = schoolCaps[school]; if (cap.mReachedLimit && spellCost <= cap.mMinCost) continue; static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, school) < fAutoSpellChance) continue; selectedSpells.push_back(spell.mId); if (cap.mReachedLimit) { 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 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); // 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 && testSpellCost < cap.mMinCost) { cap.mMinCost = testSpellCost; cap.mWeakestSpell = testSpell->mId; } } } else { cap.mCount += 1; if (cap.mCount == cap.mLimit) cap.mReachedLimit = true; if (spellCost < cap.mMinCost) { cap.mWeakestSpell = spell.mId; cap.mMinCost = spellCost; } } } return selectedSpells; } std::vector autoCalcPlayerSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race) { const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); static const float fPCbaseMagickaMult = esmStore.get().find("fPCbaseMagickaMult")->mValue.getFloat(); 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; 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; 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()) continue; if (baseMagicka < spellCost) continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->mValue.getFloat(); if (calcAutoCastChance(&spell, actorSkills, actorAttributes, {}) < fAutoPCSpellChance) continue; if (!attrSkillCheck(&spell, actorSkills, actorAttributes)) continue; selectedSpells.push_back(spell.mId); if (reachedLimit) { 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 ESM::RefId& testSpellName : selectedSpells) { const ESM::Spell* testSpell = esmStore.get().find(testSpellName); int testSpellCost = MWMechanics::calcSpellCost(*testSpell); if (testSpellCost < minCost) { minCost = testSpellCost; weakestSpell = testSpell; } } } else { if (spellCost < minCost) { weakestSpell = &spell; minCost = MWMechanics::calcSpellCost(*weakestSpell); } static const unsigned int iAutoPCSpellMax = esmStore.get().find("iAutoPCSpellMax")->mValue.getInteger(); if (selectedSpells.size() == iAutoPCSpellMax) reachedLimit = true; } } return selectedSpells; } 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().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)) { 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)) { ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute); auto found = actorAttributes.find(attribute); if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin) return false; } } return true; } 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::IndexedENAMstruct& effect : spell->mEffects.mList) { 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.mData.mMagnMin; maxMagn = effect.mData.mMagnMax; } int duration = 0; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)) duration = effect.mData.mDuration; if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce)) duration = std::max(1, duration); 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.mData.mArea) * magicEffect->mData.mBaseCost; x *= fEffectCostMult; if (effect.mData.mRange == ESM::RT_Target) x *= 1.5f; 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; effectiveSchool = magicEffect->mData.mSchool; skillTerm = s; } } } 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; if (spell->mData.mFlags & ESM::Spell::F_Always) return 100.f; float skillTerm = 0; 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 float castChance = skillTerm - MWMechanics::calcSpellCost(*spell) + 0.2f * actorAttributes.at(ESM::Attribute::Willpower).getBase() + 0.1f * actorAttributes.at(ESM::Attribute::Luck).getBase(); return castChance; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/autocalcspell.hpp000066400000000000000000000025741503074453300245420ustar00rootroot00000000000000#ifndef OPENMW_AUTOCALCSPELL_H #define OPENMW_AUTOCALCSPELL_H #include "creaturestats.hpp" #include #include #include #include namespace ESM { struct Spell; struct Race; } 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 std::vector autoCalcNpcSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race); std::vector autoCalcPlayerSpells(const std::map& actorSkills, const std::map& actorAttributes, const ESM::Race* race); // Helpers bool attrSkillCheck(const ESM::Spell* spell, const std::map& actorSkills, const std::map& actorAttributes); void calcWeakestSchool(const ESM::Spell* spell, const std::map& actorSkills, ESM::RefId& effectiveSchool, float& skillTerm); float calcAutoCastChance(const ESM::Spell* spell, const std::map& actorSkills, const std::map& actorAttributes, ESM::RefId effectiveSchool); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/character.cpp000066400000000000000000004074641503074453300236450ustar00rootroot00000000000000/* * OpenMW - The completely unofficial reimplementation of Morrowind * * This file (character.cpp) is part of the OpenMW package. * * OpenMW is distributed as free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 3, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * version 3 along with this program. If not, see * https://www.gnu.org/licenses/ . */ #include "character.hpp" #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/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.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 "security.hpp" #include "spellcasting.hpp" #include "weapontype.hpp" namespace { 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 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 {}; } } // 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 {}; } } 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; } } float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight) { 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 a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill); x = fallDistanceBase + fallDistanceMult * x; x *= a; return x; } return 0.f; } bool isRealWeapon(int weaponType) { return weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell && weaponType != ESM::Weapon::None; } } namespace MWMechanics { std::string CharacterController::chooseRandomGroup(const std::string& prefix, int* num) const { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); int numAnims = 0; while (mAnimation->hasAnimation(prefix + std::to_string(numAnims + 1))) ++numAnims; int roll = Misc::Rng::rollDice(numAnims, prng) + 1; // [1, numAnims] if (num) *num = roll; return prefix + std::to_string(roll); } void CharacterController::clearStateAnimation(std::string& anim) const { if (anim.empty()) return; if (mAnimation) mAnimation->disable(anim); anim.clear(); } void CharacterController::resetCurrentJumpState() { clearStateAnimation(mCurrentJump); mJumpState = JumpState_None; } void CharacterController::resetCurrentMovementState() { clearStateAnimation(mCurrentMovement); mMovementState = CharState_None; mMovementAnimationHasMovement = false; } void CharacterController::resetCurrentIdleState() { clearStateAnimation(mCurrentIdle); mIdleState = CharState_None; } void CharacterController::resetCurrentHitState() { clearStateAnimation(mCurrentHit); mHitState = CharState_None; } void CharacterController::resetCurrentWeaponState() { clearStateAnimation(mCurrentWeapon); mUpperBodyState = UpperBodyState::None; } void CharacterController::resetCurrentDeathState() { clearStateAnimation(mCurrentDeath); mDeathState = CharState_None; } 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); if (mPtr == getPlayer() && mHitState == CharState_Block && block) { mHitState = CharState_None; resetCurrentIdleState(); } if (!mPtr.getClass().isNpc() && mUpperBodyState > UpperBodyState::WeaponEquipped) { recovery = false; stats.setHitRecovery(false); } if (mHitState != CharState_None) { if (!mAnimation->isPlaying(mCurrentHit)) { 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; 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) { mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown; } else if (recovery) { mHitState = isSwimming ? CharState_SwimHit : CharState_Hit; priority = Priority_Hit; } else if (block) { mHitState = CharState_Block; 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 (!mCurrentWeapon.empty()) mAnimation->disable(mCurrentWeapon); if (mUpperBodyState > UpperBodyState::WeaponEquipped) { mUpperBodyState = UpperBodyState::WeaponEquipped; if (mWeaponType > ESM::Weapon::None) mAnimation->showWeapons(true); } else if (mUpperBodyState < UpperBodyState::WeaponEquipped) { mUpperBodyState = UpperBodyState::None; } } if (!mAnimation->hasAnimation(mCurrentHit)) { mCurrentHit.clear(); return; } playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f, std::numeric_limits::max()); } void CharacterController::refreshJumpAnims(JumpingState jump, bool force) { if (!force && jump == mJumpState) return; if (jump == JumpState_None) { if (!mCurrentJump.empty()) resetCurrentIdleState(); resetCurrentJumpState(); 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; clearStateAnimation(mCurrentJump); 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); } bool CharacterController::onOpen() const { if (mPtr.getType() == ESM::Container::sRecordId) { 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; } void CharacterController::onClose() const { if (mPtr.getType() == ESM::Container::sRecordId) { 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); } } std::string_view CharacterController::getWeaponAnimation(int weaponType) const { 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; const ESM::WeaponType* weapInfo = getWeaponType(weaponType); // 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"; return weaponGroup; } std::string_view CharacterController::getWeaponShortGroup(int weaponType) const { if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr)) return {}; return getWeaponType(weaponType)->mShortGroup; } std::string CharacterController::fallbackShortWeaponGroup( const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const { 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); 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; // 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; if (!mAnimation->hasAnimation(groupName)) { groupName = baseGroupName; if (blendMask != nullptr) *blendMask = MWRender::BlendMask_LowerBody; } return groupName; } void CharacterController::refreshMovementAnims(CharacterState movement, bool force) { if (movement == mMovementState && !force) return; std::string_view movementAnimGroup = movementStateToAnimGroup(movement); if (movementAnimGroup.empty()) { if (!mCurrentMovement.empty()) resetCurrentIdleState(); resetCurrentMovementState(); return; } std::string movementAnimName{ movementAnimGroup }; 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; } } MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All; std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); // Non-biped creatures don't use spellcasting-specific movement animations. if (!isRealWeapon(mWeaponType) && !mPtr.getClass().isBipedal(mPtr)) weapShortGroup = {}; if (swimpos == std::string::npos && !weapShortGroup.empty()) { std::string weapMovementAnimName; // Spellcasting stance turning is a special case if (mWeaponType == ESM::Weapon::Spell && isTurning()) { weapMovementAnimName = weapShortGroup; weapMovementAnimName += movementAnimName; } else { weapMovementAnimName = movementAnimName; weapMovementAnimName += weapShortGroup; } if (!mAnimation->hasAnimation(weapMovementAnimName)) weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask); movementAnimName = std::move(weapMovementAnimName); } if (!mAnimation->hasAnimation(movementAnimName)) { std::string::size_type runpos = movementAnimName.find("run"); if (runpos != std::string::npos) movementAnimName.replace(runpos, 3, "walk"); if (!mAnimation->hasAnimation(movementAnimName)) { if (!mCurrentMovement.empty()) resetCurrentIdleState(); resetCurrentMovementState(); return; } } // If we're playing the same animation, start it from the point it ended float startpoint = 0.f; if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement) mAnimation->getInfo(mCurrentMovement, &startpoint); mMovementAnimationHasMovement = true; clearStateAnimation(mCurrentMovement); mCurrentMovement = std::move(movementAnimName); // 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)) { CharacterState walkState = runStateToWalkState(mMovementState); std::string_view anim = movementStateToAnimGroup(walkState); 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; } } 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)); mMovementAnimationHasMovement = false; } } playBlendedAnimation(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, std::numeric_limits::max(), true); } 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)) { resetCurrentIdleState(); return; } if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty())) return; mIdleState = idle; std::string idleGroup = idleStateToAnimGroup(mIdleState); if (idleGroup.empty()) { resetCurrentIdleState(); return; } 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) { priority = getIdlePriority(CharState_Idle); idleGroup = idleStateToAnimGroup(CharState_Idle); } if (fallback || mIdleState == CharState_Idle || mIdleState == CharState_SpecialIdle) { std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); if (!weapShortGroup.empty()) { 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 auto& prng = MWBase::Environment::get().getWorld()->getPrng(); numLoops = 1 + Misc::Rng::rollDice(4, prng); } } if (!mAnimation->hasAnimation(idleGroup)) { resetCurrentIdleState(); return; } 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); 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 scripted, do not touch it if (isScriptedAnimPlaying()) return; refreshHitRecoilAnims(); refreshJumpAnims(jump, force); refreshMovementAnims(movement, force); // idle handled last as it can depend on the other states refreshIdleAnims(idle, force); } void CharacterController::playDeath(float startpoint, CharacterState death) { mDeathState = death; mCurrentDeath = deathStateToAnimGroup(mDeathState); 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. resetCurrentMovementState(); resetCurrentWeaponState(); resetCurrentHitState(); resetCurrentIdleState(); resetCurrentJumpState(); playBlendedAnimation( mCurrentDeath, Priority_Death, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } CharacterState CharacterController::chooseRandomDeathState() const { int selected = 0; chooseRandomGroup("death", &selected); return static_cast(CharState_Death1 + (selected - 1)); } 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(); } mDeathState = hitStateToDeathState(mHitState); if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr)) mDeathState = CharState_SwimDeath; if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState))) mDeathState = chooseRandomDeathState(); // Do not interrupt scripted animation by death if (isScriptedAnimPlaying()) return; playDeath(startpoint, mDeathState); } std::string CharacterController::chooseRandomAttackAnimation() const { std::string result; bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); if (isSwimming) result = chooseRandomGroup("swimattack"); if (!isSwimming || !mAnimation->hasAnimation(result)) result = chooseRandomGroup("attack"); return result; } CharacterController::CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim) : mPtr(ptr) , mAnimation(anim) { if (!mAnimation) return; mAnimation->setTextKeyListener(this); const MWWorld::Class& cls = mPtr.getClass(); if (cls.isActor()) { /* 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)) { 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 (!cls.getCreatureStats(mPtr).isDead()) { 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 = cStats.getHealth().getBase() != 0; } // else: nothing to do, will detect death in the next frame and start playing death animation } } else { /* Don't accumulate with non-actors. */ mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f)); mIdleState = CharState_Idle; } // 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); } } void CharacterController::handleTextKey( std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { std::string_view evt = key->second; MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); 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; } auto& charClass = mPtr.getClass(); if (evt.substr(0, 10) == "soundgen: ") { std::string_view soundgen = evt.substr(10); // The event can optionally contain volume and pitch modifiers float volume = 1.0f; float pitch = 1.0f; if (soundgen.find(' ') != std::string::npos) { std::vector tokens; Misc::StringUtils::split(soundgen, tokens); soundgen = tokens[0]; if (tokens.size() >= 2) { volume = Misc::StringUtils::toNumeric(tokens[1], volume); } if (tokens.size() >= 3) { pitch = Misc::StringUtils::toNumeric(tokens[2], pitch); } } 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; } std::string_view action = evt.substr(groupname.size() + 2); if (action == "equip attach") { if (mUpperBodyState == UpperBodyState::Equipping) { if (groupname == "shield") mAnimation->showCarriedLeft(true); else mAnimation->showWeapons(true); } } else if (action == "unequip detach") { if (mUpperBodyState == UpperBodyState::Unequipping) { if (groupname == "shield") mAnimation->showCarriedLeft(false); else mAnimation->showWeapons(false); } } else if (action == "chop hit" || action == "slash hit" || action == "thrust hit" || action == "hit") { int attackType = -1; if (action == "hit") { 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 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 (mReadyToHit) { charClass.hit(mPtr, mAttackStrength, attackType, mAttackVictim, mAttackHitPos, mAttackSuccess); mReadyToHit = false; } } else if (isRandomAttackAnimation(groupname) && action == "start") { std::multimap::const_iterator hitKey = key; // 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 (mReadyToHit) { mAnimation->releaseArrow(mAttackStrength); mReadyToHit = false; } } 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); } void CharacterController::updatePtr(const MWWorld::Ptr& ptr) { mPtr = ptr; } void CharacterController::updateIdleStormState(bool inwater) const { if (!mAnimation->hasAnimation("idlestorm")) return; bool animPlaying = mAnimation->isPlaying("idlestorm"); if (mUpperBodyState != UpperBodyState::None || inwater) { 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) { 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 (animPlaying) { mAnimation->setLoopingEnabled("idlestorm", 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); } float CharacterController::calculateWindUp() const { if (mCurrentWeapon.empty() || mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon)) return -1.f; 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; return std::clamp( (mAnimation->getCurrentTime(mCurrentWeapon) - minAttackTime) / (maxAttackTime - minAttackTime), 0.f, 1.f); } void CharacterController::prepareHit() { if (mReadyToHit) 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(); } mReadyToHit = true; } bool CharacterController::updateWeaponState() { // If the current animation is scripted, we can't do anything here. if (isScriptedAnimPlaying()) return false; const auto world = MWBase::Environment::get().getWorld(); auto& prng = world->getPrng(); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); 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(); const ESM::RefId* downSoundId = nullptr; bool weaponChanged = false; bool ammunition = true; float weapSpeed = 1.f; if (cls.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); MWWorld::Ptr newWeapon; if (weapon != inv.end()) { 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 (mWeapon != newWeapon) { mWeapon = newWeapon; weaponChanged = true; } 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) { mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperBodyState::WeaponEquipped; } setAttackingOrSpell(false); } } MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && updateCarriedLeftVisible(mWeaponType)) { if (mAnimation->isPlaying("shield")) mAnimation->disable("shield"); 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->disable("torch"); } } MWRender::Animation::AnimPriority priorityWeapon(Priority_Default); if (cls.isBipedal(mPtr)) { // For bipeds, blend weapon animations with lower body animations with higher priority // For non-bipeds, movement takes priority priorityWeapon = Priority_Weapon; priorityWeapon[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; } bool forcestateupdate = false; // We should not play equipping animation and sound during weapon->weapon transition const bool isStillWeapon = isRealWeapon(mWeaponType) && isRealWeapon(weaptype); // 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) { forcestateupdate = true; if (!mCurrentWeapon.empty()) mAnimation->disable(mCurrentWeapon); mUpperBodyState = UpperBodyState::WeaponEquipped; setAttackingOrSpell(false); mAnimation->showWeapons(true); } if (!isKnockedOut() && !isKnockedDown() && !isRecovery()) { std::string weapgroup; if (((!isWerewolf && cls.isBipedal(mPtr)) || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType && mUpperBodyState <= UpperBodyState::AttackWindUp && mUpperBodyState != UpperBodyState::Unequipping && !isStillWeapon) { // We can not play un-equip animation if weapon changed since last update if (!weaponChanged) { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); int unequipMask = MWRender::BlendMask_All; mUpperBodyState = UpperBodyState::Unequipping; 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->disable(weapgroup); playBlendedAnimation( weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); 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 (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 && mUpperBodyState <= UpperBodyState::WeaponEquipped) { forcestateupdate = true; bool useShieldAnims = mAnimation->useShieldAnimations(); if (!useShieldAnims) mAnimation->showCarriedLeft(updateCarriedLeftVisible(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); if (!isStillWeapon) { if (animPlaying) mAnimation->disable(mCurrentWeapon); if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); int equipMask = MWRender::BlendMask_All; mUpperBodyState = UpperBodyState::Equipping; 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); } if (weaptype != ESM::Weapon::Spell || cls.isBipedal(mPtr)) { playBlendedAnimation(weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 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 = 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; } // Make sure that we disabled unequipping animation if (mUpperBodyState == UpperBodyState::Unequipping) { resetCurrentWeaponState(); mWeaponType = ESM::Weapon::None; } } } if (isWerewolf) { 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); } float complete = 0.f; bool animPlaying = false; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; if (getAttackingOrSpell()) { bool resetIdle = true; if (mUpperBodyState == UpperBodyState::WeaponEquipped && (mHitState == CharState_None || mHitState == CharState_Block)) { mAttackStrength = -1.f; mReadyToHit = false; // Randomize attacks for non-bipedal creatures if (!cls.isBipedal(mPtr) && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon))) { mCurrentWeapon = chooseRandomAttackAnimation(); } if (mWeaponType == ESM::Weapon::Spell) { // 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 = cls.getInventoryStore(mPtr); if (inv.getSelectedEnchantItem() != inv.end()) { const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem(); spellid = enchantItem.getClass().getEnchantment(enchantItem); isMagicItem = true; } } if (isMagicItem && !Settings::game().mUseMagicItemAnimations) { 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; } // 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) { world->breakInvisibility(mPtr); MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell); 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 const ESM::Static* castStatic = world->getStore().get().find(ESM::RefId::stringRefId("VFX_Hands")); const VFS::Path::Normalized castStaticModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); if (mAnimation->getNode("Bip01 L Hand")) mAnimation->addEffect( castStaticModel.value(), "", false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) mAnimation->addEffect( castStaticModel.value(), "", 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"; 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 { resetIdle = false; } } else { 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 (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) mAttackType = "shoot"; else if (mPtr == getPlayer()) { if (Settings::game().mBestAttack) { 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 { mAttackType = getMovementBasedAttackType(); } } else if (aiInactive) { mAttackType = getDesiredAttackType(); if (mAttackType == "") mAttackType = getRandomAttackType(); } // else if (mPtr != getPlayer()) use mAttackType set by AiCombat startKey = mAttackType + ' ' + startKey; stopKey = mAttackType + " max 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); } } // We should not break swim and sneak animations if (resetIdle && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim) { resetCurrentIdleState(); } } // 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())) { 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(); 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 { prepareHit(); } 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) { std::string hit = mAttackType != "shoot" ? "hit" : "release"; float startPoint = 0.f; // Skip a bit of the pre-hit section based on the attack strength if (minAttackTime != -1.f && minAttackTime < maxAttackTime) { 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); } 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") { std::string strength = mAttackStrength < 0.33f ? "small" : mAttackStrength < 0.66f ? "medium" : "large"; start = strength + ' ' + start; stop = strength + ' ' + stop; } mReadyToHit = false; if (animPlaying) mAnimation->disable(mCurrentWeapon); playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0); mUpperBodyState = UpperBodyState::AttackEnd; animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); } } if (!animPlaying) animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if (!animPlaying || complete >= 1.f) { if (mUpperBodyState == UpperBodyState::Equipping || mUpperBodyState == UpperBodyState::AttackEnd || mUpperBodyState == UpperBodyState::Casting) { 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->setPitchFactor(0.f); if (mUpperBodyState > UpperBodyState::WeaponEquipped && (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)) { mAnimation->setPitchFactor(1.f); // 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)); else mAnimation->setPitchFactor(1.f - complete); } } mAnimation->setAccurateAiming(mUpperBodyState > UpperBodyState::WeaponEquipped); return forcestateupdate; } void CharacterController::updateAnimQueue() { if (mAnimQueue.empty()) return; if (!mAnimation->isPlaying(mAnimQueue.front().mGroup)) { // 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)) { mAnimation->setPlayScriptedOnly(false); mAnimation->disable(mAnimQueue.front().mGroup); mAnimQueue.pop_front(); shouldPlayOrRestart = true; } else // 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); if (shouldPlayOrRestart) { // Move on to the remaining items of the queue playAnimQueue(); } } else { float complete; size_t loopcount; mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount); mAnimQueue.front().mLoopCount = loopcount; mAnimQueue.front().mTime = complete; } if (!mAnimQueue.empty()) mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1); } void CharacterController::playAnimQueue(bool loopStart) { if (!mAnimQueue.empty()) { 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); } } 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; updateMagicEffects(); bool isPlayer = mPtr == MWMechanics::getPlayer(); bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson(); bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState(); float scale = mPtr.getCellRef().getScale(); if (!Settings::game().mNormaliseRaceSpeed && cls.isNpc()) { 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; } if (cls.isActor() && cls.getCreatureStats(mPtr).wasTeleported()) { mSmoothedSpeed = osg::Vec2f(); cls.getCreatureStats(mPtr).setTeleported(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; } osg::Vec3f rot = cls.getRotationVector(mPtr); osg::Vec3f vec(movementSettings.asVec3()); movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); vec.normalize(); const bool smoothMovement = Settings::game().mSmoothMovement; if (smoothMovement) { 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) { 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 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) { 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; } 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 (isKnockedOut() || isKnockedDown() || isRecovery() || isScriptedAnimPlaying()) vec = osg::Vec3f(); CharacterState movestate = CharState_None; CharacterState idlestate = CharState_None; JumpingState jumpstate = JumpState_None; const MWWorld::Store& gmst = world->getStore().get(); if (vec.x() != 0.f || vec.y() != 0.f) { // advance athletics if (isPlayer) { 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; } } } if (!godmode) { // 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; } if (!inwater && !flying && solid) { // In the air (either getting up —ascending part of jump— or falling). if (!onground) { 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; } // Started a jump. else if (mJumpState != JumpState_InAir && vec.z() > 0.f) { 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 (!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); if (healthLost > 0.0f) { const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm(); // 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(); } 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 (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; } } } if (turnToMovementDirection && !isFirstPersonPlayer && isBiped && (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 += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta); mAnimation->setBodyPitchRadians(swimmingPitch); } else mAnimation->setBodyPitchRadians(0); if (inwater && isPlayer && !isFirstPersonPlayer && Settings::game().mSwimUpwardCorrection) { const float swimUpwardCoef = Settings::game().mSwimUpwardCoef; vec.z() = std::abs(vec.y()) * swimUpwardCoef; vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef); } // Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering if (isPlayer) { float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f; float complete; bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete); if (movestate == CharState_None && jumpstate == JumpState_None && isTurning()) { if (animPlaying && complete < threshold) movestate = mMovementState; } } else { if (isBiped) { if (mTurnAnimationThreshold > 0) mTurnAnimationThreshold -= duration; if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft || movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft) { mTurnAnimationThreshold = 0.05f; } else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0) { movestate = mMovementState; } } } if (movestate != CharState_None) { clearAnimQueue(); jumpstate = JumpState_None; } updateAnimQueue(); if (!mAnimQueue.empty()) idlestate = CharState_SpecialIdle; else if (sneak && !mInJump) idlestate = CharState_IdleSneak; else idlestate = CharState_Idle; if (inwater) idlestate = CharState_IdleSwim; if (!mSkipAnim) { refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState()); updateIdleStormState(inwater); } 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) { // 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 (!mSkipAnim) { if (!isKnockedDown() && !isKnockedOut()) { if (rot != osg::Vec3f()) world->rotateObject(mPtr, rot, true); } else // avoid z-rotating for knockdown { if (rot.x() != 0 && rot.y() != 0) { rot.z() = 0.0f; world->rotateObject(mPtr, rot, true); } } updateHeadTracking(duration); } movement = vec; movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0; // 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 if (cls.getCreatureStats(mPtr).isDead()) { // 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); } } osg::Vec3f movementFromAnimation = mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration); if (mPtr.getClass().isActor() && !isScriptedAnimPlaying()) { if (isMovementAnimationControlled()) { 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; } movement = movementFromAnimation; } else { movement = osg::Vec3f(); } } else if (mSkipAnim) { movement = osg::Vec3f(); } if (mFloatToSurface && world->isSwimming(mPtr)) { if (cls.getCreatureStats(mPtr).isDead() || (!godmode && cls.getCreatureStats(mPtr) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Paralyze) .getModifier() > 0)) { movement.z() = 1.0; } } movement.x() *= scale; movement.y() *= scale; world->queueMovement(mPtr, movement); } mSkipAnim = false; mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead()); } void CharacterController::persistAnimationState() const { ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { // TODO: Probably want to presist lua animations too if (!iter->mScripted) continue; ESM::AnimationState::ScriptedAnimation anim; anim.mGroup = iter->mGroup; 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; } state.mScriptedAnims.push_back(anim); } } void CharacterController::unpersistAnimationState() { const ESM::AnimationState& state = mPtr.getRefData().getAnimationState(); if (!state.mScriptedAnims.empty()) { 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(); } } 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); } bool CharacterController::playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted) { if (!mAnimation || !mAnimation->hasAnimation(groupname)) return false; // We should not interrupt scripted animations with non-scripted ones if (isScriptedAnimPlaying() && !scripted) return true; bool looping = mAnimation->isLoopingAnimation(groupname); // 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"); if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": stop"); if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop)) { mAnimQueue.resize(1); return true; } } // 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--; 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"; bool playImmediately = false; if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup)) { clearAnimQueue(scripted); playImmediately = true; } else { mAnimQueue.resize(1); } mAnimQueue.push_back(entry); if (playImmediately) playAnimQueue(mode == 2); return true; } bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey, std::string_view stopKey, uint32_t loops, bool forceLoop) { // 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 (!mAnimation || !mAnimation->hasAnimation(groupname)) return false; 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); mAnimQueue.push_back(entry); if (mAnimQueue.size() == 1) playAnimQueue(); return true; } void CharacterController::enableLuaAnimations(bool enable) { mLuaAnimations = enable; } void CharacterController::skipAnim() { mSkipAnim = true; } bool CharacterController::isScriptedAnimPlaying() const { // 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; } bool CharacterController::isAnimPlaying(std::string_view groupName) const { if (mAnimation == nullptr) return false; return mAnimation->isPlaying(groupName); } bool CharacterController::isMovementAnimationControlled() const { if (mHitState != CharState_None) return true; if (Settings::game().mPlayerMovementIgnoresAnimation && mPtr == getPlayer()) return false; if (mInJump) return false; bool movementAnimationControlled = mIdleState != CharState_None; if (mMovementState != CharState_None) movementAnimationControlled = mMovementAnimationHasMovement; return movementAnimationControlled; } 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); if (clearScriptedAnims) { mAnimation->setPlayScriptedOnly(false); mAnimQueue.clear(); return; } for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();) { if (!it->mScripted) it = mAnimQueue.erase(it); else ++it; } } 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); if (mDeathState != CharState_None) { playRandomDeath(); } updateAnimQueue(); mAnimation->runAnimation(0.f); } CharacterController::KillResult CharacterController::kill() { if (mDeathState == CharState_None) { playRandomDeath(); resetCurrentIdleState(); 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; } void CharacterController::resurrect() { if (mDeathState == CharState_None) return; resetCurrentDeathState(); 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. // Stop any effects that are no longer active std::vector effects = mAnimation->getLoopingEffects(); for (std::string_view effectId : effects) { auto index = ESM::MagicEffect::indexNameToIndex(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() const { if (!mPtr.getClass().isActor()) return; 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; bool vampire = mPtr.getClass() .getCreatureStats(mPtr) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Vampirism) .getMagnitude() > 0.0f; mAnimation->setVampire(vampire); } void CharacterController::setVisibility(float visibility) const { // We should take actor's invisibility in account if (mPtr.getClass().isActor()) { 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); } // TODO: implement a dithering shader rather than just change object transparency. mAnimation->setAlpha(visibility); } 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::isRandomAttackAnimation(std::string_view group) { return (group == "attack1" || group == "swimattack1" || group == "attack2" || group == "swimattack2" || group == "attack3" || group == "swimattack3"); } bool CharacterController::isAttackPreparing() const { return mUpperBodyState == UpperBodyState::AttackWindUp; } bool CharacterController::isCastingSpell() const { return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting; } bool CharacterController::isReadyToBlock() const { return updateCarriedLeftVisible(mWeaponType); } bool CharacterController::isKnockedDown() const { return mHitState == CharState_KnockDown || mHitState == CharState_SwimKnockDown; } bool CharacterController::isKnockedOut() const { return mHitState == CharState_KnockOut || mHitState == CharState_SwimKnockOut; } bool CharacterController::isTurning() const { return mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight || mMovementState == CharState_SwimTurnLeft || mMovementState == CharState_SwimTurnRight; } bool CharacterController::isRecovery() const { return mHitState == CharState_Hit || mHitState == CharState_SwimHit; } bool CharacterController::isAttackingOrSpell() const { return mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped; } bool CharacterController::isSneaking() const { return mIdleState == CharState_IdleSneak || mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack || mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight; } 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::setAttackingOrSpell(bool attackingOrSpell) const { mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell); } void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell) { setAttackingOrSpell(true); mCastingScriptedSpell = scriptedSpell; ActionSpell action = ActionSpell(spellId); action.prepare(mPtr); } void CharacterController::setAIAttackType(std::string_view attackType) { mAttackType = attackType; } 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::readyToPrepareAttack() const { return (mHitState == CharState_None || mHitState == CharState_Block) && mUpperBodyState <= UpperBodyState::WeaponEquipped; } bool CharacterController::readyToStartAttack() const { if (mHitState != CharState_None && mHitState != CharState_Block) return false; return mUpperBodyState == UpperBodyState::WeaponEquipped; } float CharacterController::getAttackStrength() const { return mAttackStrength; } bool CharacterController::getAttackingOrSpell() const { return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell(); } std::string_view CharacterController::getDesiredAttackType() const { return mPtr.getClass().getCreatureStats(mPtr).getAttackType(); } void CharacterController::setActive(int active) const { mAnimation->setActive(active); } void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr& target) { mHeadTrackTarget = target; } void CharacterController::playSwishSound() const { 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; const MWWorld::Class& cls = mPtr.getClass(); if (cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf()) { 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; } if (!soundId->empty()) MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch); } 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; } } void CharacterController::updateHeadTracking(float duration) { const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (!head) return; double zAngleRadians = 0.f; double xAngleRadians = 0.f; 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; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/character.hpp000066400000000000000000000235611503074453300236420ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_CHARACTER_HPP #define GAME_MWMECHANICS_CHARACTER_HPP #include #include #include "../mwworld/ptr.hpp" #include "../mwrender/animation.hpp" namespace MWWorld { class InventoryStore; } namespace MWRender { class Animation; } 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_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 { None, Equipping, Unequipping, WeaponEquipped, AttackWindUp, AttackRelease, AttackEnd, Casting }; 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 { 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 }; CharacterState mIdleState{ CharState_None }; std::string mCurrentIdle; CharacterState mMovementState{ CharState_None }; std::string mCurrentMovement; float mMovementAnimSpeed{ 0.f }; bool mAdjustMovementAnimSpeed{ false }; bool mMovementAnimationHasMovement{ false }; CharacterState mDeathState{ CharState_None }; std::string mCurrentDeath; bool mFloatToSurface{ true }; CharacterState mHitState{ CharState_None }; std::string mCurrentHit; UpperBodyState mUpperBodyState{ UpperBodyState::None }; JumpingState mJumpState{ JumpState_None }; std::string mCurrentJump; bool mInJump{ false }; int mWeaponType{ ESM::Weapon::None }; std::string mCurrentWeapon; float mAttackStrength{ -1.f }; bool mReadyToHit{ false }; MWWorld::Ptr mAttackVictim; osg::Vec3f mAttackHitPos; bool mAttackSuccess{ false }; bool mSkipAnim{ false }; // counted for skill increase float mSecondsOfSwimming{ 0.f }; float mSecondsOfRunning{ 0.f }; MWWorld::ConstPtr mHeadTrackTarget; float mTurnAnimationThreshold{ 0.f }; // how long to continue playing turning animation after actor stopped turning std::string mAttackType; // slash, chop or thrust bool mCanCast{ false }; bool mCastingScriptedSpell{ false }; bool mIsMovingBackward{ false }; osg::Vec2f mSmoothedSpeed; std::string_view getMovementBasedAttackType() const; void clearStateAnimation(std::string& anim) const; void resetCurrentJumpState(); void resetCurrentMovementState(); void resetCurrentIdleState(); void resetCurrentHitState(); void resetCurrentWeaponState(); void resetCurrentDeathState(); 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); bool updateWeaponState(); void updateIdleStormState(bool inwater) const; std::string chooseRandomAttackAnimation() const; static bool isRandomAttackAnimation(std::string_view group); bool isMovementAnimationControlled() const; void updateAnimQueue(); void playAnimQueue(bool useLoopStart = false); void updateHeadTracking(float duration); void updateMagicEffects() const; void playDeath(float startpoint, CharacterState death); CharacterState chooseRandomDeathState() const; void playRandomDeath(float startpoint = 0.0f); /// 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; bool updateCarriedLeftVisible(int weaptype) const; std::string fallbackShortWeaponGroup( const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr) const; std::string_view getWeaponAnimation(int weaponType) const; std::string_view getWeaponShortGroup(int weaponType) const; bool getAttackingOrSpell() const; void setAttackingOrSpell(bool attackingOrSpell) const; std::string_view getDesiredAttackType() const; void prepareHit(); public: CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); virtual ~CharacterController(); 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; }; } #endif /* GAME_MWMECHANICS_CHARACTER_HPP */ openmw-openmw-0.49.0/apps/openmw/mwmechanics/combat.cpp000066400000000000000000001041771503074453300231510ustar00rootroot00000000000000 #include "combat.hpp" #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/globals.hpp" #include "../mwworld/inventorystore.hpp" #include "actorutil.hpp" #include "difficultyscaling.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "pathfinding.hpp" #include "spellcasting.hpp" #include "spellresistance.hpp" namespace { 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) { const ESM::RefId enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ESM::RefId(); if (!enchantmentName.empty()) { 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) { if (!blocker.getClass().hasInventoryStore(blocker)) return false; MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); if (blockerStats.getKnockedDown() // Used for both knockout or knockdown || blockerStats.getHitRecovery() || blockerStats.isParalyzed()) return false; if (!MWBase::Environment::get().getMechanicsManager()->isReadyToBlock(blocker)) return false; MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); 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))); const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); static const float fCombatBlockLeftAngle = gmst.find("fCombatBlockLeftAngle")->mValue.getFloat(); if (angleDegrees < fCombatBlockLeftAngle) return false; 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() + 0.1f * blockerStats.getAttribute(ESM::Attribute::Luck).getModified(); float enemySwing = attackStrength; 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) { static const float fBlockStillBonus = gmst.find("fBlockStillBonus")->mValue.getFloat(); blockerTerm *= fBlockStillBonus; } blockerTerm *= blockerStats.getFatigueTerm(); float attackerSkill = 0; if (weapon.isEmpty()) attackerSkill = attacker.getClass().getSkill(attacker, ESM::Skill::HandToHand); 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(); attackerTerm *= attackerStats.getFatigueTerm(); 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); 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); // Reduce blocker fatigue 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); float fatigueLoss = fFatigueBlockBase + normalizedEncumbrance * fFatigueBlockMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * attackStrength * fWeaponFatigueBlockMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); blockerStats.setFatigue(fatigue); blockerStats.setBlock(true); if (blocker == getPlayer()) blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, ESM::Skill::Block_Success); return true; } return false; } bool isNormalWeapon(const MWWorld::Ptr& weapon) { if (weapon.isEmpty()) return false; const int flags = weapon.get()->mBase->mData.mFlags; bool isSilver = flags & ESM::Weapon::Silver; bool isMagical = flags & ESM::Weapon::Magical; bool isEnchanted = !weapon.getClass().getEnchantment(weapon).empty(); return !isSilver && !isMagical && (!isEnchanted || !Settings::game().mEnchantedWeaponsAreMagical); } void resistNormalWeapon( const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage) { if (weapon.isEmpty() || !isNormalWeapon(weapon)) return; const MWMechanics::MagicEffects& effects = actor.getClass().getCreatureStats(actor).getMagicEffects(); 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); 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) { if (damage == 0 || weapon.isEmpty() || !actor.getClass().isNpc()) return; const int flags = weapon.get()->mBase->mData.mFlags; bool isSilver = flags & ESM::Weapon::Silver; if (isSilver && actor.getClass().getNpcStats(actor).isWerewolf()) { 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) { 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 skillValue = attacker.getClass().getSkill(attacker, weaponSkill); if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue)) { victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false, MWMechanics::DamageSourceType::Ranged); MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker); return; } { 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); } reduceWeaponCondition(damage, validVictim, weapon, attacker); 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, ESM::Skill::Weapon_SuccessfulHit); const MWMechanics::AiSequence& sequence = victim.getClass().getCreatureStats(victim).getAiSequence(); bool unaware = attacker == getPlayer() && !sequence.isInCombat() && !MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim); bool knockedDown = victim.getClass().getCreatureStats(victim).getKnockedDown(); if (knockedDown || unaware) { static const float fCombatKODamageMult = gmst.find("fCombatKODamageMult")->mValue.getFloat(); damage *= fCombatKODamageMult; if (!knockedDown) MWBase::Environment::get().getSoundManager()->playSound3D( victim, ESM::RefId::stringRefId("critical damage"), 1.0f, 1.0f); } } // Apply "On hit" effect of the projectile bool appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition, true); if (validVictim) { // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory if (victim != getPlayer() && !appliedEnchantment) { 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, MWMechanics::DamageSourceType::Ranged); } } 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(); 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)) { defenseTerm = victimStats.getEvasion(); } static const float fCombatInvisoMult = gmst.find("fCombatInvisoMult")->mValue.getFloat(); defenseTerm += std::min(100.f, fCombatInvisoMult * victimStats.getMagicEffects().getOrDefault(ESM::MagicEffect::Chameleon).getMagnitude()); defenseTerm += std::min(100.f, 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); attackTerm *= stats.getFatigueTerm(); 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) { // Don't let elemental shields harm the player in god mode. bool godmode = attacker == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (godmode) return; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); for (int i = 0; i < 3; ++i) { 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(); float fatigueMax = attackerStats.getFatigue().getModified(); float fatigueCurrent = attackerStats.getFatigue().getCurrent(); 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(prng)); int element = ESM::MagicEffect::FireDamage; if (i == 1) element = ESM::MagicEffect::ShockDamage; if (i == 2) element = ESM::MagicEffect::FrostDamage; float elementResistance = MWMechanics::getEffectResistanceAttribute(element, &attackerStats.getMagicEffects()); x = std::min(100.f, x + elementResistance); 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. x = scaleDamage(x, victim, attacker); MWMechanics::DynamicStat health = attackerStats.getHealth(); health.setCurrent(health.getCurrent() - x); attackerStats.setHealth(health); 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) { if (weapon.isEmpty()) return; if (!hit) damage = 0.f; const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); if (weaphashealth) { int weaphealth = weapon.getClass().getItemHealth(weapon); 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() .getESMStore() ->get() .find("fWeaponDamageMult") ->mValue.getFloat(); float x = std::max(1.f, fWeaponDamageMult * damage); weaphealth -= std::min(int(x), weaphealth); weapon.getCellRef().setCharge(weaphealth); } // Weapon broken? unequip it if (weaphealth == 0) weapon = *attacker.getClass().getInventoryStore(attacker).unequipItem(weapon); } } void adjustWeaponDamage(float& damage, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker) { if (weapon.isEmpty()) return; const bool weaphashealth = weapon.getClass().hasItemHealth(weapon); if (weaphashealth) { damage *= weapon.getClass().getItemNormalizedHealth(weapon); } 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) { 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(); 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. 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) { healthdmg = true; // GLOB instead of GMST because it gets updated during a quest damage *= MWBase::Environment::get().getWorld()->getGlobalFloat(MWWorld::Globals::sWerewolfClawMult); } if (healthdmg) { static const float fHandtoHandHealthPer = store.get().find("fHandtoHandHealthPer")->mValue.getFloat(); damage *= fHandtoHandHealthPer; } MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (isWerewolf) { 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, ESM::RefId::stringRefId("Hand To Hand Hit"), 1.0f, 1.0f); } 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().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); bool godmode = attacker == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (!godmode) { float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; if (!weapon.isEmpty()) fatigueLoss += weapon.getClass().getWeight(weapon) * attackStrength * fWeaponFatigueMult; fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); stats.setFatigue(fatigue); } } float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2) { 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() .getESMStore() ->get() .find("iFightDistanceBase") ->mValue.getInteger(); static const float fFightDistanceMultiplier = MWBase::Environment::get() .getESMStore() ->get() .find("fFightDistanceMultiplier") ->mValue.getFloat(); return (iFightDistanceBase - fFightDistanceMultiplier * d); } 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; } float getMeleeWeaponReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon) { MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Store& store = world->getStore().get(); const float fCombatDistance = store.find("fCombatDistance")->mValue.getFloat(); if (!weapon.isEmpty()) return fCombatDistance * weapon.get()->mBase->mData.mReach; if (actor.getClass().isNpc()) return fCombatDistance * store.find("fHandToHandReach")->mValue.getFloat(); return fCombatDistance; } bool isInMeleeReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const float reach) { const float heightDiff = actor.getRefData().getPosition().pos[2] - target.getRefData().getPosition().pos[2]; return std::abs(heightDiff) < reach && getDistanceToBounds(actor, target) < reach; } 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); if (dist >= minDist || !isInMeleeReach(actor, target, reach)) continue; const osg::Vec3f targetPos(target.getRefData().getPosition().asVec3()); // 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; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/combat.hpp000066400000000000000000000066561503074453300231610ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_COMBAT_H #define OPENMW_MECHANICS_COMBAT_H #include namespace osg { class Vec3f; } namespace MWWorld { class Ptr; } namespace MWMechanics { 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 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 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); /// 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); /// @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); 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); float getFightDistanceBias(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2); float getAggroDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); // Cursed distance calculation used for combat proximity and hit checks in Morrowind float getDistanceToBounds(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); float getMeleeWeaponReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& weapon); bool isInMeleeReach(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const float reach); // 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/creaturecustomdataresetter.hpp000066400000000000000000000006211503074453300273530ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/creaturestats.cpp000066400000000000000000000447621503074453300246000ustar00rootroot00000000000000#include "creaturestats.hpp" #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWMechanics { int CreatureStats::sActorId = 0; CreatureStats::CreatureStats() { for (const ESM::Attribute& attribute : MWBase::Environment::get().getESMStore()->get()) { mAttributes.emplace(attribute.mId, AttributeValue{}); } } const AiSequence& CreatureStats::getAiSequence() const { return mAiSequence; } AiSequence& CreatureStats::getAiSequence() { return mAiSequence; } float CreatureStats::getFatigueTerm() const { float max = getFatigue().getModified(); float current = getFatigue().getCurrent(); float normalised = std::floor(max) == 0 ? 1 : std::max(0.0f, current / max); 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); } const AttributeValue& CreatureStats::getAttribute(ESM::RefId id) const { return mAttributes.at(id); } const DynamicStat& CreatureStats::getHealth() const { return mDynamic[0]; } const DynamicStat& CreatureStats::getMagicka() const { return mDynamic[1]; } const DynamicStat& CreatureStats::getFatigue() const { return mDynamic[2]; } const Spells& CreatureStats::getSpells() const { return mSpells; } const ActiveSpells& CreatureStats::getActiveSpells() const { return mActiveSpells; } const MagicEffects& CreatureStats::getMagicEffects() const { return mMagicEffects; } int CreatureStats::getLevel() const { return mLevel; } Stat CreatureStats::getAiSetting(AiSetting index) const { return mAiSettings[static_cast>(index)]; } const DynamicStat& CreatureStats::getDynamic(int index) const { if (index < 0 || index > 2) { throw std::runtime_error("dynamic stat index is out of range"); } return mDynamic[index]; } Spells& CreatureStats::getSpells() { return mSpells; } ActiveSpells& CreatureStats::getActiveSpells() { return mActiveSpells; } MagicEffects& CreatureStats::getMagicEffects() { return mMagicEffects; } void CreatureStats::setAttribute(ESM::RefId id, float base) { AttributeValue current = getAttribute(id); current.setBase(base); setAttribute(id, current); } void CreatureStats::setAttribute(ESM::RefId id, const AttributeValue& value) { const AttributeValue& currentValue = mAttributes.at(id); if (value != currentValue) { 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(); DynamicStat fatigue = getFatigue(); float currentToBaseRatio = fatigue.getBase() > 0 ? (fatigue.getCurrent() / fatigue.getBase()) : 0; 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) { setDynamic(0, value); } void CreatureStats::setMagicka(const DynamicStat& value) { setDynamic(1, value); } void CreatureStats::setFatigue(const DynamicStat& value) { setDynamic(2, 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 (!mDead) mTimeOfDeath = MWBase::Environment::get().getWorld()->getTimeStamp(); mDead = true; mDynamic[index].setCurrent(0); } } void CreatureStats::setLevel(int level) { mLevel = level; } void CreatureStats::setAiSetting(AiSetting index, Stat value) { mAiSettings[static_cast>(index)] = value; } void CreatureStats::setAiSetting(AiSetting index, int base) { Stat stat = getAiSetting(index); stat.setBase(base); setAiSetting(index, stat); } bool CreatureStats::isParalyzed() const { 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 { return mDead; } bool CreatureStats::isDeathAnimationFinished() const { return mDeathAnimationFinished; } void CreatureStats::setDeathAnimationFinished(bool finished) { mDeathAnimationFinished = finished; } void CreatureStats::notifyDied() { mDied = true; } bool CreatureStats::hasDied() const { return mDied; } void CreatureStats::clearHasDied() { mDied = false; } bool CreatureStats::hasBeenMurdered() const { return mMurdered; } void CreatureStats::notifyMurder() { mMurdered = true; } void CreatureStats::clearHasBeenMurdered() { mMurdered = false; } void CreatureStats::resurrect() { if (mDead) { mDynamic[0].setCurrent(mDynamic[0].getBase()); mDead = false; mDeathAnimationFinished = false; } } bool CreatureStats::hasCommonDisease() const { return mSpells.hasCommonDisease(); } bool CreatureStats::hasBlightDisease() const { return mSpells.hasBlightDisease(); } int CreatureStats::getFriendlyHits() const { return mFriendlyHits; } void CreatureStats::friendlyHit() { ++mFriendlyHits; } void CreatureStats::resetFriendlyHits() { mFriendlyHits = 0; } bool CreatureStats::hasTalkedToPlayer() const { return mTalkedTo; } void CreatureStats::talkedToPlayer() { mTalkedTo = true; } bool CreatureStats::isAlarmed() const { return mAlarmed; } void CreatureStats::setAlarmed(bool alarmed) { mAlarmed = alarmed; } bool CreatureStats::getAttacked() const { return mAttacked; } 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); evasion *= getFatigueTerm(); evasion += std::min(100.f, mMagicEffects.getOrDefault(ESM::MagicEffect::Sanctuary).getMagnitude()); return evasion; } void CreatureStats::setLastHitObject(const ESM::RefId& objectid) { mLastHitObject = objectid; } void CreatureStats::clearLastHitObject() { mLastHitObject = ESM::RefId(); } const ESM::RefId& CreatureStats::getLastHitObject() const { return mLastHitObject; } void CreatureStats::setLastHitAttemptObject(const ESM::RefId& objectid) { mLastHitAttemptObject = objectid; } void CreatureStats::clearLastHitAttemptObject() { mLastHitAttemptObject = ESM::RefId(); } const ESM::RefId& CreatureStats::getLastHitAttemptObject() const { return mLastHitAttemptObject; } void CreatureStats::setHitAttemptActorId(int actorId) { mHitAttemptActorId = actorId; } int CreatureStats::getHitAttemptActorId() const { return mHitAttemptActorId; } void CreatureStats::addToFallHeight(float height) { mFallHeight += height; } float CreatureStats::getFallHeight() const { return mFallHeight; } float CreatureStats::land(bool isPlayer) { if (isPlayer) MWBase::Environment::get().getWorld()->getPlayer().setJumping(false); float height = mFallHeight; mFallHeight = 0; return height; } void CreatureStats::recalculateMagicka() { auto world = MWBase::Environment::get().getWorld(); float intelligence = getAttribute(ESM::Attribute::Intelligence).getModified(); 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 setKnockedDownOverOneFrame(false); } bool CreatureStats::getKnockedDown() const { return mKnockdown; } void CreatureStats::setKnockedDownOneFrame(bool value) { mKnockdownOneFrame = value; } bool CreatureStats::getKnockedDownOneFrame() const { return mKnockdownOneFrame; } void CreatureStats::setKnockedDownOverOneFrame(bool value) { mKnockdownOverOneFrame = value; } bool CreatureStats::getKnockedDownOverOneFrame() const { return mKnockdownOverOneFrame; } void CreatureStats::setHitRecovery(bool value) { mHitRecovery = value; } bool CreatureStats::getHitRecovery() const { return mHitRecovery; } void CreatureStats::setBlock(bool value) { mBlock = value; } bool CreatureStats::getBlock() const { return mBlock; } bool CreatureStats::getMovementFlag(Flag flag) const { return (mMovementFlags & flag) != 0; } void CreatureStats::setMovementFlag(Flag flag, bool state) { if (state) mMovementFlags |= flag; else mMovementFlags &= ~flag; } bool CreatureStats::getStance(Stance flag) const { switch (flag) { case Stance_Run: return getMovementFlag(Flag_Run) || getMovementFlag(Flag_ForceRun); case Stance_Sneak: return getMovementFlag(Flag_Sneak) || getMovementFlag(Flag_ForceSneak); default: return false; } } DrawState CreatureStats::getDrawState() const { return mDrawState; } void CreatureStats::setDrawState(DrawState state) { mDrawState = state; } void CreatureStats::writeState(ESM::CreatureStats& state) const { for (size_t i = 0; i < state.mAttributes.size(); ++i) getAttribute(ESM::Attribute::indexToRefId(i)).writeState(state.mAttributes[i]); for (size_t i = 0; i < state.mDynamic.size(); ++i) mDynamic[i].writeState(state.mDynamic[i]); state.mTradeTime = mLastRestock.toEsm(); state.mGoldPool = mGoldPool; state.mDead = mDead; state.mDeathAnimationFinished = mDeathAnimationFinished; state.mDied = mDied; state.mMurdered = mMurdered; // The vanilla engine does not store friendly hits in the save file. Since there's no other mechanism // that ever resets the friendly hits (at least not to my knowledge) this should be regarded a feature // rather than a bug. // state.mFriendlyHits = mFriendlyHits; state.mTalkedTo = mTalkedTo; state.mAlarmed = mAlarmed; state.mAttacked = mAttacked; // TODO: rewrite. does this really need 3 separate bools? state.mKnockdown = mKnockdown; state.mKnockdownOneFrame = mKnockdownOneFrame; state.mKnockdownOverOneFrame = mKnockdownOverOneFrame; state.mHitRecovery = mHitRecovery; state.mBlock = mBlock; state.mMovementFlags = mMovementFlags; state.mFallHeight = mFallHeight; // TODO: vertical velocity (move from PhysicActor to CreatureStats?) state.mLastHitObject = mLastHitObject; state.mLastHitAttemptObject = mLastHitAttemptObject; state.mRecalcDynamicStats = false; state.mDrawState = static_cast(mDrawState); state.mLevel = mLevel; state.mActorId = mActorId; state.mDeathAnimation = mDeathAnimation; state.mTimeOfDeath = mTimeOfDeath.toEsm(); // state.mHitAttemptActorId = mHitAttemptActorId; mSpells.writeState(state.mSpells); mActiveSpells.writeState(state.mActiveSpells); mAiSequence.writeState(state.mAiSequence); mMagicEffects.writeState(state.mMagicEffects); state.mSummonedCreatures = mSummonedCreatures; state.mSummonGraveyard = mSummonGraveyard; state.mHasAiSettings = true; for (size_t i = 0; i < state.mAiSettings.size(); ++i) mAiSettings[i].writeState(state.mAiSettings[i]); state.mMissingACDT = false; } void CreatureStats::readState(const ESM::CreatureStats& state) { if (!state.mMissingACDT) { for (size_t i = 0; i < state.mAttributes.size(); ++i) mAttributes[ESM::Attribute::indexToRefId(i)].readState(state.mAttributes[i]); for (size_t i = 0; i < state.mDynamic.size(); ++i) mDynamic[i].readState(state.mDynamic[i]); mGoldPool = state.mGoldPool; mTalkedTo = state.mTalkedTo; mAttacked = state.mAttacked; } mLastRestock = MWWorld::TimeStamp(state.mTradeTime); mDead = state.mDead; mDeathAnimationFinished = state.mDeathAnimationFinished; mDied = state.mDied; mMurdered = state.mMurdered; mAlarmed = state.mAlarmed; // TODO: rewrite. does this really need 3 separate bools? mKnockdown = state.mKnockdown; mKnockdownOneFrame = state.mKnockdownOneFrame; mKnockdownOverOneFrame = state.mKnockdownOverOneFrame; mHitRecovery = state.mHitRecovery; mBlock = state.mBlock; mMovementFlags = state.mMovementFlags; mFallHeight = state.mFallHeight; mLastHitObject = state.mLastHitObject; mLastHitAttemptObject = state.mLastHitAttemptObject; mDrawState = DrawState(state.mDrawState); mLevel = state.mLevel; mActorId = state.mActorId; mDeathAnimation = state.mDeathAnimation; mTimeOfDeath = MWWorld::TimeStamp(state.mTimeOfDeath); // mHitAttemptActorId = state.mHitAttemptActorId; mSpells.readState(state.mSpells, this); mActiveSpells.readState(state.mActiveSpells); mAiSequence.readState(state.mAiSequence); mMagicEffects.readState(state.mMagicEffects); mSummonedCreatures = state.mSummonedCreatures; mSummonGraveyard = state.mSummonGraveyard; if (state.mHasAiSettings) for (size_t i = 0; i < state.mAiSettings.size(); ++i) mAiSettings[i].readState(state.mAiSettings[i]); if (state.mRecalcDynamicStats) recalculateMagicka(); } void CreatureStats::setLastRestockTime(MWWorld::TimeStamp tradeTime) { mLastRestock = tradeTime; } MWWorld::TimeStamp CreatureStats::getLastRestockTime() const { return mLastRestock; } void CreatureStats::setGoldPool(int pool) { mGoldPool = pool; } int CreatureStats::getGoldPool() const { return mGoldPool; } int CreatureStats::getActorId() { if (mActorId == -1) mActorId = sActorId++; return mActorId; } bool CreatureStats::matchesActorId(int id) const { return mActorId != -1 && id == mActorId; } void CreatureStats::cleanup() { sActorId = 0; } 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) { esm.getHNT(sActorId, "COUN"); } signed char CreatureStats::getDeathAnimation() const { return mDeathAnimation; } void CreatureStats::setDeathAnimation(signed char index) { mDeathAnimation = index; } MWWorld::TimeStamp CreatureStats::getTimeOfDeath() const { return mTimeOfDeath; } std::multimap& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } std::vector& CreatureStats::getSummonedCreatureGraveyard() { return mSummonGraveyard; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/creaturestats.hpp000066400000000000000000000222761503074453300246010ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_CREATURESTATS_H #define GAME_MWMECHANICS_CREATURESTATS_H #include #include #include #include #include "activespells.hpp" #include "aisequence.hpp" #include "aisetting.hpp" #include "drawstate.hpp" #include "magiceffects.hpp" #include "spells.hpp" #include "stat.hpp" #include #include #include namespace ESM { struct CreatureStats; } namespace MWMechanics { struct CorprusStats { static constexpr int sWorseningPeriod = 24; int mWorsenings[ESM::Attribute::Length]; MWWorld::TimeStamp mNextWorsening; }; /// \brief Common creature stats /// /// class CreatureStats { static int sActorId; std::map mAttributes; DynamicStat mDynamic[3]; // health, magicka, fatigue DrawState mDrawState = DrawState::Nothing; Spells mSpells; ActiveSpells mActiveSpells; MagicEffects mMagicEffects; Stat mAiSettings[4]; AiSequence mAiSequence; bool mDead = false; bool mDeathAnimationFinished = false; bool mDied = false; // flag for OnDeath script function bool mMurdered = false; int mFriendlyHits = 0; bool mTalkedTo = false; bool mAlarmed = false; bool mAttacked = false; bool mKnockdown = false; bool mKnockdownOneFrame = false; bool mKnockdownOverOneFrame = false; bool mHitRecovery = false; bool mBlock = false; unsigned int mMovementFlags = 0; float mFallHeight = 0.f; 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; // The pool of merchant gold (not in inventory) int mGoldPool = 0; int mActorId = -1; // Stores an actor that attacked this actor. Only one is stored at a time, and it is not changed if a different // actor attacks. It is cleared when combat ends. int mHitAttemptActorId = -1; // The difference between view direction and lower body direction. float mSideMovementAngle = 0; MWWorld::TimeStamp mTimeOfDeath; private: 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; protected: std::string mAttackType; int mLevel = 0; bool mAttackingOrSpell = false; private: // The index of the death animation that was played, or -1 if none played signed char mDeathAnimation = -1; bool mTeleported = false; public: CreatureStats(); DrawState getDrawState() const; void setDrawState(DrawState state); void recalculateMagicka(); float getFallHeight() const; void addToFallHeight(float height); /// Reset the fall height /// @return total fall height float land(bool isPlayer = false); const AttributeValue& getAttribute(ESM::RefId id) const; const DynamicStat& getHealth() const; const DynamicStat& getMagicka() const; const DynamicStat& getFatigue() const; const DynamicStat& getDynamic(int index) const; const Spells& getSpells() const; const ActiveSpells& getActiveSpells() const; const MagicEffects& getMagicEffects() const; bool getAttackingOrSpell() const { return mAttackingOrSpell; } std::string_view getAttackType() const { return mAttackType; } int getLevel() const; Spells& getSpells(); ActiveSpells& getActiveSpells(); MagicEffects& getMagicEffects(); void setAttribute(ESM::RefId id, const AttributeValue& value); // Shortcut to set only the base void setAttribute(ESM::RefId id, float base); void setHealth(const DynamicStat& value); void setMagicka(const DynamicStat& value); void setFatigue(const DynamicStat& value); void setDynamic(int index, const DynamicStat& value); void setAttackingOrSpell(bool attackingOrSpell) { mAttackingOrSpell = attackingOrSpell; } void setAttackType(std::string_view attackType) { mAttackType = attackType; } void setLevel(int level); void setAiSetting(AiSetting index, Stat value); void setAiSetting(AiSetting index, int base); Stat getAiSetting(AiSetting index) const; const AiSequence& getAiSequence() const; AiSequence& getAiSequence(); float getFatigueTerm() const; ///< Return effective fatigue bool isParalyzed() const; bool isDead() const; bool isDeathAnimationFinished() const; void setDeathAnimationFinished(bool finished); void notifyDied(); bool hasDied() const; void clearHasDied(); bool hasBeenMurdered() const; void clearHasBeenMurdered(); void notifyMurder(); void resurrect(); bool hasCommonDisease() const; bool hasBlightDisease() const; int getFriendlyHits() const; ///< Number of friendly hits received. 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); bool getAttacked() const; void setAttacked(bool attacked); float getEvasion() const; void setKnockedDown(bool value); /// Returns true for the entire duration of the actor being knocked down or knocked out, /// 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 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 bool getKnockedDownOverOneFrame() const; void setHitRecovery(bool value); bool getHitRecovery() const; void setBlock(bool value); bool getBlock() const; std::multimap& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds enum Flag { Flag_ForceRun = 1, Flag_ForceSneak = 2, Flag_Run = 4, Flag_Sneak = 8, Flag_ForceJump = 16, Flag_ForceMoveJump = 32 }; enum Stance { Stance_Run, Stance_Sneak }; 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 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; void writeState(ESM::CreatureStats& state) const; void readState(const ESM::CreatureStats& state); static void writeActorIdCounter(ESM::ESMWriter& esm); static void readActorIdCounter(ESM::ESMReader& esm); void setLastRestockTime(MWWorld::TimeStamp tradeTime); MWWorld::TimeStamp getLastRestockTime() const; void setGoldPool(int pool); int getGoldPool() const; signed char getDeathAnimation() const; // -1 means not decided void setDeathAnimation(signed char index); MWWorld::TimeStamp getTimeOfDeath() const; int getActorId(); ///< Will generate an actor ID, if the actor does not have one yet. 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(); 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; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/damagesourcetype.hpp000066400000000000000000000003611503074453300252400ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H #define OPENMW_MWMECHANICS_DAMAGESOURCETYPE_H namespace MWMechanics { enum class DamageSourceType { Unspecified, Melee, Ranged, Magical, }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/difficultyscaling.cpp000066400000000000000000000020031503074453300253700ustar00rootroot00000000000000#include "difficultyscaling.hpp" #include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "actorutil.hpp" float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim) { const MWWorld::Ptr& player = MWMechanics::getPlayer(); static const float fDifficultyMult = MWBase::Environment::get().getESMStore()->get().find("fDifficultyMult")->mValue.getFloat(); const float difficultyTerm = 0.01f * Settings::game().mDifficulty; float x = 0; if (victim == player) { if (difficultyTerm > 0) x = fDifficultyMult * difficultyTerm; else x = difficultyTerm / fDifficultyMult; } else if (attacker == player) { if (difficultyTerm > 0) x = -difficultyTerm / fDifficultyMult; else x = fDifficultyMult * (-difficultyTerm); } damage *= 1 + x; return damage; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/difficultyscaling.hpp000066400000000000000000000004501503074453300254010ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_DIFFICULTYSCALING_H #define OPENMW_MWMECHANICS_DIFFICULTYSCALING_H namespace MWWorld { class Ptr; } /// Scales damage dealt to an actor based on difficulty setting float scaleDamage(float damage, const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim); #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/disease.hpp000066400000000000000000000070471503074453300233240ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_DISEASE_H #define OPENMW_MECHANICS_DISEASE_H #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/ptr.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "spells.hpp" 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(const MWWorld::Ptr& actor, const MWWorld::Ptr& carrier) { if (!carrier.getClass().isActor() || actor != getPlayer()) return; float fDiseaseXferChance = MWBase::Environment::get() .getESMStore() ->get() .find("fDiseaseXferChance") ->mValue.getFloat(); const MagicEffects& actorEffects = actor.getClass().getCreatureStats(actor).getMagicEffects(); Spells& spells = carrier.getClass().getCreatureStats(carrier).getSpells(); for (const ESM::Spell* spell : spells) { if (actor.getClass().getCreatureStats(actor).getSpells().hasSpell(spell->mId)) continue; float resist = 0.f; if (Spells::hasCorprusEffect(spell)) 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.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.getOrDefault(ESM::MagicEffect::ResistBlightDisease).getMagnitude() - actorEffects.getOrDefault(ESM::MagicEffect::WeaknessToBlightDisease).getMagnitude()); else continue; int x = static_cast(fDiseaseXferChance * 100 * resist); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (Misc::Rng::rollDice(10000, prng) < x) { // Contracted disease! actor.getClass().getCreatureStats(actor).getSpells().add(spell); MWBase::Environment::get().getWorld()->applyLoopingParticles(actor); 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); } } } } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/drawstate.hpp000066400000000000000000000003171503074453300236760ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_DRAWSTATE_H #define GAME_MWMECHANICS_DRAWSTATE_H namespace MWMechanics { enum class DrawState { Nothing = 0, Weapon = 1, Spell = 2 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/enchanting.cpp000066400000000000000000000350301503074453300240110ustar00rootroot00000000000000#include "enchanting.hpp" #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics { Enchanting::Enchanting() : mCastStyle(ESM::Enchantment::CastOnce) , mSelfEnchanting(false) , mObjectType(0) , mWeaponType(-1) { } void Enchanting::setOldItem(const MWWorld::Ptr& oldItem) { mOldItemPtr = oldItem; mWeaponType = -1; mObjectType = 0; if (!itemEmpty()) { mObjectType = mOldItemPtr.getType(); if (mObjectType == ESM::Weapon::sRecordId) mWeaponType = mOldItemPtr.get()->mBase->mData.mType; } } void Enchanting::setNewItemName(const std::string& s) { mNewItemName = s; } void Enchanting::setEffect(const ESM::EffectList& effectList) { mEffectList = effectList; } int Enchanting::getCastStyle() const { return mCastStyle; } void Enchanting::setSoulGem(const MWWorld::Ptr& soulGem) { mSoulGemPtr = soulGem; } bool Enchanting::create() { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); ESM::Enchantment enchantment; enchantment.mData.mFlags = 0; enchantment.mData.mType = mCastStyle; enchantment.mData.mCost = getBaseCastCost(); enchantment.mRecordFlags = 0; store.remove(mSoulGemPtr, 1); // 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) { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (getEnchantChance() <= (Misc::Rng::roll0to99(prng))) return false; mEnchanter.getClass().skillUsageSucceeded( mEnchanter, ESM::Skill::Enchant, ESM::Skill::Enchant_CreateMagicItem); } enchantment.mEffects = mEffectList; int count = getEnchantItemsCount(); if (mCastStyle == ESM::Enchantment::ConstantEffect) enchantment.mData.mCharge = 0; else enchantment.mData.mCharge = getGemCharge() / count; // 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().getESMStore()->insert(enchantment); // Apply the enchantment const ESM::RefId& newItemId = mOldItemPtr.getClass().applyEnchantment(mOldItemPtr, enchantmentPtr->mId, getGemCharge(), mNewItemName); if (!mSelfEnchanting) payForEnchantment(count); // 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() .getESMStore() ->get() .find("iSoulAmountForConstantEffect") ->mValue.getInteger(); if ((mObjectType == ESM::Armor::sRecordId) || (mObjectType == ESM::Clothing::sRecordId)) { // Armor or Clothing switch (mCastStyle) { case ESM::Enchantment::WhenUsed: if (powerfulSoul) mCastStyle = ESM::Enchantment::ConstantEffect; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; return; } } else if (mWeaponType != -1) { // Weapon ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; switch (mCastStyle) { case ESM::Enchantment::WhenStrikes: if (weapclass == ESM::WeaponType::Melee || weapclass == ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenUsed; return; case ESM::Enchantment::WhenUsed: if (powerfulSoul && weapclass != ESM::WeaponType::Ammo && weapclass != ESM::WeaponType::Thrown) mCastStyle = ESM::Enchantment::ConstantEffect; else if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; default: // takes care of Constant effect too mCastStyle = ESM::Enchantment::WhenUsed; if (weapclass != ESM::WeaponType::Ranged) mCastStyle = ESM::Enchantment::WhenStrikes; return; } } else if (mObjectType == ESM::Book::sRecordId) { // Scroll or Book mCastStyle = ESM::Enchantment::CastOnce; return; } // Fail case mCastStyle = ESM::Enchantment::CastOnce; } /* * Vanilla enchant cost formula: * * Touch/Self: (min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025 * Target: 1.5 * ((min + max) * baseCost * 0.025 * duration + area * baseCost * 0.025) * Constant eff: (min + max) * baseCost * 2.5 + area * baseCost * 0.025 * * For multiple effects - cost of each effect is multiplied by number of effects that follows +1. * * Note: Minimal value inside formula for 'min' and 'max' is 1. So in vanilla: * (0 + 0) == (1 + 0) == (1 + 1) => 2 or (2 + 0) == (1 + 2) => 3 * * Formula on UESPWiki is not entirely correct. */ float Enchanting::getEnchantPoints(bool precise) const { if (mEffectList.mList.empty()) // No effects added, cost = 0 return 0; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const float fEffectCostMult = store.get().find("fEffectCostMult")->mValue.getFloat(); const float fEnchantmentConstantDurationMult = store.get().find("fEnchantmentConstantDurationMult")->mValue.getFloat(); float enchantmentCost = 0.f; float cost = 0.f; for (const ESM::IndexedENAMstruct& effect : mEffectList.mList) { 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; cost += ((magMin + magMax) * duration + area) * baseCost * fEffectCostMult * 0.05f; cost = std::max(1.f, cost); if (effect.mData.mRange == ESM::RT_Target) cost *= 1.5f; enchantmentCost += precise ? cost : std::floor(cost); } return enchantmentCost; } const ESM::Enchantment* Enchanting::getRecord(const ESM::Enchantment& toFind) const { 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) continue; // Don't choose an ID that came from the content files, would have unintended side effects if (!enchantments.isDynamic(iter->mId)) continue; bool mismatch = false; for (int i = 0; i < static_cast(iter->mEffects.mList.size()); ++i) { if (iter->mEffects.mList[i] != toFind.mEffects.mList[i]) { mismatch = true; break; } } if (!mismatch) return &(*iter); } return nullptr; } int Enchanting::getBaseCastCost() const { if (mCastStyle == ESM::Enchantment::ConstantEffect) return 0; return static_cast(getEnchantPoints(false)); } int Enchanting::getEffectiveCastCost() const { int baseCost = getBaseCastCost(); MWWorld::Ptr player = getPlayer(); return getEffectiveEnchantmentCastCost(static_cast(baseCost), player); } int Enchanting::getEnchantPrice(int count) const { if (mEnchanter.isEmpty()) return 0; 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().getESMStore(); if (soulEmpty()) return 0; if (mSoulGemPtr.getCellRef().getSoul().empty()) return 0; const ESM::Creature* soul = store.get().search(mSoulGemPtr.getCellRef().getSoul()); if (soul) return soul->mData.mSoul; else return 0; } int Enchanting::getMaxEnchantValue() const { if (itemEmpty()) return 0; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); return static_cast(mOldItemPtr.getClass().getEnchantmentPoints(mOldItemPtr) * store.get().find("fEnchantmentMult")->mValue.getFloat()); } bool Enchanting::soulEmpty() const { return mSoulGemPtr.isEmpty(); } bool Enchanting::itemEmpty() const { return mOldItemPtr.isEmpty(); } void Enchanting::setSelfEnchanting(bool selfEnchanting) { mSelfEnchanting = selfEnchanting; } void Enchanting::setEnchanter(const MWWorld::Ptr& enchanter) { mEnchanter = enchanter; // Reset cast style mCastStyle = ESM::Enchantment::CastOnce; } int Enchanting::getEnchantChance() const { const CreatureStats& stats = mEnchanter.getClass().getCreatureStats(mEnchanter); 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 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(); if (mCastStyle == ESM::Enchantment::ConstantEffect) x *= fEnchantmentConstantChanceMult; return static_cast(x); } int Enchanting::getEnchantItemsCount() const { int count = 1; float enchantPoints = getEnchantPoints(); if (mWeaponType != -1 && enchantPoints > 0) { ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(mWeaponType)->mWeaponClass; if (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo) { MWWorld::Ptr player = getPlayer(); count = player.getClass().getContainerStore(player).count(mOldItemPtr.getCellRef().getRefId()); count = std::clamp( getGemCharge() * Settings::game().mProjectilesEnchantMultiplier / enchantPoints, 1, count); } } return count; } float Enchanting::getTypeMultiplier() const { 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) return 0.125f; } return 1.f; } void Enchanting::payForEnchantment(int count) const { const MWWorld::Ptr& player = getPlayer(); MWWorld::ContainerStore& store = player.getClass().getContainerStore(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() + price); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/enchanting.hpp000066400000000000000000000040751503074453300240230ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_ENCHANTING_H #define GAME_MWMECHANICS_ENCHANTING_H #include #include #include #include "../mwworld/ptr.hpp" namespace MWMechanics { class Enchanting { 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/greetingstate.hpp000066400000000000000000000003361503074453300245460ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_GREETINGSTATE_H #define OPENMW_MWMECHANICS_GREETINGSTATE_H namespace MWMechanics { enum GreetingState { Greet_None, Greet_InProgress, Greet_Done }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/inventory.hpp000066400000000000000000000022241503074453300237340ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/levelledlist.cpp000066400000000000000000000056241503074453300243710ustar00rootroot00000000000000#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); } } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/levelledlist.hpp000066400000000000000000000006771503074453300244010ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_LEVELLEDLIST_H #define OPENMW_MECHANICS_LEVELLEDLIST_H #include #include namespace ESM { struct LevelledListBase; class RefId; } namespace MWMechanics { /// @return ID of resulting item, or empty if none ESM::RefId getLevelledItem( const ESM::LevelledListBase* levItem, bool creature, Misc::Rng::Generator& prng, std::optional level = {}); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/magiceffects.cpp000066400000000000000000000146331503074453300243210ustar00rootroot00000000000000#include "magiceffects.hpp" #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) { } EffectKey::EffectKey(const ESM::ENAMstruct& effect) { mId = effect.mEffectID; mArg = ESM::Skill::indexToRefId(effect.mSkill); ESM::RefId attribute = ESM::Attribute::indexToRefId(effect.mAttribute); if (!attribute.empty()) { if (!mArg.empty()) throw std::runtime_error("magic effect can't have both a skill and an attribute argument"); mArg = attribute; } } 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.mId < right.mId) return true; if (left.mId > right.mId) return false; return left.mArg < right.mArg; } bool operator==(const EffectKey& left, const EffectKey& right) { return left.mId == right.mId && left.mArg == right.mArg; } float EffectParam::getMagnitude() const { return mBase + mModifier; } void EffectParam::modifyBase(int diff) { mBase += diff; } int EffectParam::getBase() const { return mBase; } void EffectParam::setBase(int base) { mBase = base; } void EffectParam::setModifier(float mod) { mModifier = mod; } float EffectParam::getModifier() const { return mModifier; } EffectParam::EffectParam() : mModifier(0) , mBase(0) { } EffectParam& EffectParam::operator+=(const EffectParam& param) { mModifier += param.mModifier; mBase += param.mBase; truncate(mModifier); return *this; } EffectParam& EffectParam::operator-=(const EffectParam& param) { mModifier -= param.mModifier; mBase -= param.mBase; truncate(mModifier); return *this; } void MagicEffects::add(const EffectKey& key, const EffectParam& param) { Collection::iterator iter = mCollection.find(key); if (iter == mCollection.end()) { mCollection.insert(std::make_pair(key, param)); } else { iter->second += param; } } void MagicEffects::modifyBase(const EffectKey& key, int diff) { mCollection[key].modifyBase(diff); } EffectParam MagicEffects::getOrDefault(const EffectKey& key) const { return get(key).value_or(EffectParam()); } std::optional MagicEffects::get(const EffectKey& key) const { Collection::const_iterator iter = mCollection.find(key); if (iter != mCollection.end()) { return iter->second; } return std::nullopt; } void MagicEffects::writeState(ESM::MagicEffects& state) const { for (const auto& [key, params] : mCollection) { if (params.getBase() != 0 || params.getModifier() != 0.f) { // Don't worry about mArg, never used by magic effect script instructions state.mEffects[key.mId] = { params.getBase(), params.getModifier() }; } } } void MagicEffects::readState(const ESM::MagicEffects& state) { 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) { spellLine += ' '; spellLine += attribute->mName; } return spellLine; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/magiceffects.hpp000066400000000000000000000055211503074453300243220ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_MAGICEFFECTS_H #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 { struct EffectKey { int mId; ESM::RefId mArg; // skill or ability EffectKey(); EffectKey(int id, ESM::RefId arg = {}) : mId(id) , mArg(arg) { } 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); struct EffectParam { private: // Note usually this would be int, but applying partial resistance might introduce a decimal point. float mModifier; int mBase; public: /// Get the total magnitude including base and modifier. float getMagnitude() const; void setModifier(float mod); float getModifier() const; /// Change mBase by \a diff void modifyBase(int diff); void setBase(int base); int getBase() const; EffectParam(); EffectParam(float magnitude) : mModifier(magnitude) , mBase(0) { } EffectParam& operator+=(const EffectParam& param); EffectParam& operator-=(const EffectParam& param); }; inline EffectParam operator+(const EffectParam& left, const EffectParam& right) { EffectParam param(left); return param += right; } inline EffectParam operator-(const EffectParam& left, const EffectParam& right) { EffectParam param(left); return param -= right; } /// \brief Effects currently affecting a NPC or creature class MagicEffects { public: typedef std::map Collection; private: Collection mCollection; public: Collection::const_iterator begin() const { return mCollection.begin(); } Collection::const_iterator end() const { return mCollection.end(); } void readState(const ESM::MagicEffects& state); void writeState(ESM::MagicEffects& state) const; void add(const EffectKey& key, const EffectParam& param); void modifyBase(const EffectKey& key, int diff); 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. }; std::string getMagicEffectString( const ESM::MagicEffect& effect, const ESM::Attribute* attribute, const ESM::Skill* skill); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp000066400000000000000000002363701503074453300257000ustar00rootroot00000000000000#include "mechanicsmanagerimp.hpp" #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/player.hpp" #include "../mwworld/ptr.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/statemanager.hpp" #include "../mwbase/windowmanager.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 "autocalcspell.hpp" #include "combat.hpp" #include "npcstats.hpp" #include "spellutil.hpp" namespace { float getFightDispositionBias(float disposition) { 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) { 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 repTerm = stats.getReputation() * gmst.find("fReputationMod")->mValue.getFloat(); float fatigueTerm = stats.getFatigueTerm(); float levelTerm = stats.getLevel() * gmst.find("fLevelMod")->mValue.getFloat(); rating1 = (repTerm + luckTerm + persTerm + stats.getSkill(ESM::Skill::Speechcraft).getModified()) * fatigueTerm; if (player) { rating2 = rating1 + levelTerm; rating3 = (stats.getSkill(ESM::Skill::Mercantile).getModified() + luckTerm + persTerm) * fatigueTerm; } else { 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 { void MechanicsManager::buildPlayer() { MWWorld::Ptr ptr = getPlayer(); MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); MWMechanics::NpcStats& npcStats = ptr.getClass().getNpcStats(ptr); npcStats.recalculateMagicka(); const ESM::NPC* player = ptr.get()->mBase; // reset creatureStats.setLevel(player->mNpdt.mLevel); creatureStats.getSpells().clear(true); 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); bool male = (player->mFlags & ESM::NPC::Female) == 0; for (const ESM::Attribute& attribute : esmStore.get()) creatureStats.setAttribute(attribute.mId, race->mData.getAttribute(attribute.mId, male)); 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; npcStats.getSkill(skill.mId).setBase(5 + bonus); } for (const ESM::RefId& power : race->mPowers.mList) { creatureStats.getSpells().add(power); } } // birthsign const ESM::RefId& signId = MWBase::Environment::get().getWorld()->getPlayer().getBirthSign(); if (!signId.empty()) { const ESM::BirthSign* sign = esmStore.get().find(signId); for (const ESM::RefId& power : sign->mPowers.mList) { creatureStats.getSpells().add(power); } } // class if (mClassSelected) { const ESM::Class* class_ = esmStore.get().find(player->mClass); for (int attribute : class_->mData.mAttribute) { 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) { int bonus = i == 0 ? 10 : 25; for (const auto& skills : class_->mData.mSkills) { ESM::RefId id = ESM::Skill::indexToRefId(skills[i]); if (!id.empty()) npcStats.getSkill(id).setBase(npcStats.getSkill(id).getBase() + bonus); } } for (const ESM::Skill& skill : esmStore.get()) { if (skill.mData.mSpecialization == class_->mData.mSpecialization) npcStats.getSkill(skill.mId).setBase(npcStats.getSkill(skill.mId).getBase() + 5); } } // F_PCStart spells const ESM::Race* race = nullptr; if (mRaceSelected) race = esmStore.get().find(player->mRace); npcStats.updateHealth(); std::vector selectedSpells = autoCalcPlayerSpells(npcStats.getSkills(), npcStats.getAttributes(), race); for (const ESM::RefId& spell : selectedSpells) creatureStats.getSpells().add(spell); // forced update and current value adjustments mActors.updateActor(ptr, 0); for (int i = 0; i < 3; ++i) { 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 MWWorld::InventoryStore& invStore = ptr.getClass().getInventoryStore(ptr); for (int i = 0; i < MWWorld::InventoryStore::Slots; ++i) invStore.unequipAll(); invStore.autoEquip(); } MechanicsManager::MechanicsManager() : mUpdatePlayer(true) , mClassSelected(false) , mRaceSelected(false) , mAI(true) { // buildPlayer no longer here, needs to be done explicitly after all subsystems are up and running } void MechanicsManager::add(const MWWorld::Ptr& ptr) { if (ptr.getClass().isActor()) mActors.addActor(ptr); else mObjects.addObject(ptr); } void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) { if (ptr.getClass().isActor()) mActors.castSpell(ptr, spellId, scriptedSpell); } void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive) { if (ptr == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(MWWorld::Ptr()); mActors.removeActor(ptr, keepActive); mObjects.removeObject(ptr); } void MechanicsManager::updateCell(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) { if (old == MWBase::Environment::get().getWindowManager()->getWatchedActor()) MWBase::Environment::get().getWindowManager()->watchActor(ptr); if (ptr.getClass().isActor()) mActors.updateActor(old, ptr); else mObjects.updateObject(old, ptr); } void MechanicsManager::drop(const MWWorld::CellStore* cellStore) { mActors.dropActors(cellStore, getPlayer()); mObjects.dropObjects(cellStore); } 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(); MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); // Update the selected spell icon MWWorld::ContainerStoreIterator enchantItem = inv.getSelectedEnchantItem(); if (enchantItem != inv.end()) winMgr->setSelectedEnchantItem(*enchantItem); else { 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, true); mActors.addActor(ptr, true); } mActors.update(duration, paused); mObjects.update(duration, paused); } void MechanicsManager::processChangedSettings(const Settings::CategorySettingVector& changed) { for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Game" && it->second == "actors processing range") { int state = MWBase::Environment::get().getStateManager()->getState(); if (state != MWBase::StateManager::State_Running) continue; // Update mechanics for new processing range immediately update(0.f, false); } } } void MechanicsManager::notifyDied(const MWWorld::Ptr& actor) { mActors.notifyDied(actor); } bool MechanicsManager::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { return mActors.isActorDetected(actor, observer); } bool MechanicsManager::isAttackPreparing(const MWWorld::Ptr& ptr) { return mActors.isAttackPreparing(ptr); } bool MechanicsManager::isRunning(const MWWorld::Ptr& ptr) { return mActors.isRunning(ptr); } bool MechanicsManager::isSneaking(const MWWorld::Ptr& ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWBase::World* world = MWBase::Environment::get().getWorld(); bool animActive = mActors.isSneaking(ptr); bool stanceOn = stats.getStance(MWMechanics::CreatureStats::Stance_Sneak); bool inair = !world->isOnGround(ptr) && !world->isSwimming(ptr) && !world->isFlying(ptr); return stanceOn && (animActive || inair); } void MechanicsManager::rest(double hours, bool sleep) { if (sleep) MWBase::Environment::get().getWorld()->rest(hours); mActors.rest(hours, sleep); } void MechanicsManager::restoreDynamicStats(const MWWorld::Ptr& actor, double hours, bool sleep) { mActors.restoreDynamicStats(actor, hours, sleep); } int MechanicsManager::getHoursToRest() const { return mActors.getHoursToRest(getPlayer()); } void MechanicsManager::setPlayerName(const std::string& name) { MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mName = name; world->getStore().insert(player); mUpdatePlayer = true; } void MechanicsManager::setPlayerRace( const ESM::RefId& race, bool male, const ESM::RefId& head, const ESM::RefId& hair) { MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; 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->getStore().insert(player); world->renderPlayer(); } mRaceSelected = true; buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerBirthsign(const ESM::RefId& id) { MWBase::Environment::get().getWorld()->getPlayer().setBirthSign(id); buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerClass(const ESM::RefId& id) { MWBase::World* world = MWBase::Environment::get().getWorld(); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = id; world->getStore().insert(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } void MechanicsManager::setPlayerClass(const ESM::Class& cls) { MWBase::World* world = MWBase::Environment::get().getWorld(); const ESM::Class* ptr = world->getStore().insert(cls); ESM::NPC player = *world->getPlayerPtr().get()->mBase; player.mClass = ptr->mId; world->getStore().insert(player); mClassSelected = true; buildPlayer(); mUpdatePlayer = true; } int MechanicsManager::getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp) { 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 MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); static const float fDispRaceMod = gmst.find("fDispRaceMod")->mValue.getFloat(); 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); float reaction = 0; int rank = 0; 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)); rank = playerStats.getFactionRanks().find(npcFaction)->second; } } else if (!npcFaction.empty()) { auto playerFactionIt = playerStats.getFactionRanks().begin(); for (; playerFactionIt != playerStats.getFactionRanks().end(); ++playerFactionIt) { 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); if (playerFactionIt == playerStats.getFactionRanks().begin() || itReaction < reaction) { reaction = static_cast(itReaction); rank = playerFactionIt->second; } } } else { reaction = 0; rank = 0; } 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; static const float fDispCrimeMod = gmst.find("fDispCrimeMod")->mValue.getFloat(); static const float fDispDiseaseMod = gmst.find("fDispDiseaseMod")->mValue.getFloat(); x -= fDispCrimeMod * playerStats.getBounty(); if (playerStats.hasCommonDisease() || playerStats.hasBlightDisease()) x += fDispDiseaseMod; static const float fDispWeaponDrawn = gmst.find("fDispWeaponDrawn")->mValue.getFloat(); if (playerStats.getDrawState() == MWMechanics::DrawState::Weapon) x += fDispWeaponDrawn; x += ptr.getClass() .getCreatureStats(ptr) .getMagicEffects() .getOrDefault(ESM::MagicEffect::Charm) .getMagnitude(); if (clamp) return std::clamp(x, 0, 100); //, normally clamped to [0..100] when used return static_cast(x); } int MechanicsManager::getBarterOffer(const MWWorld::Ptr& ptr, int basePrice, bool buying) { // Make sure zero base price items/services can't be bought/sold for 1 gold // and return the intended base price for creature merchants if (basePrice == 0 || ptr.getType() == ESM::Creature::sRecordId) return basePrice; const MWMechanics::NpcStats& sellerStats = ptr.getClass().getNpcStats(ptr); MWWorld::Ptr playerPtr = getPlayer(); 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... 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); float c = std::min(0.2f * playerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); float d = std::min(ptr.getClass().getSkill(ptr, ESM::Skill::Mercantile), 100.f); float e = std::min(0.1f * sellerStats.getAttribute(ESM::Attribute::Luck).getModified(), 10.f); float f = std::min(0.2f * sellerStats.getAttribute(ESM::Attribute::Personality).getModified(), 10.f); float pcTerm = (clampedDisposition - 50 + a + b + c) * playerStats.getFatigueTerm(); float npcTerm = (d + e + f) * sellerStats.getFatigueTerm(); float buyTerm = 0.01f * (100 - 0.5f * (pcTerm - npcTerm)); float sellTerm = 0.01f * (50 - 0.5f * (npcTerm - pcTerm)); int offerPrice = int(basePrice * (buying ? buyTerm : sellTerm)); return std::max(1, offerPrice); } int MechanicsManager::countDeaths(const ESM::RefId& id) const { return mActors.countDeaths(id); } void MechanicsManager::getPersuasionDispositionChange( const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) { 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); float npcRating1, npcRating2, npcRating3; getPersuasionRatings(npcStats, npcRating1, npcRating2, npcRating3, false); float playerRating1, playerRating2, playerRating3; getPersuasionRatings(playerStats, playerRating1, playerRating2, playerRating3, true); 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(); float target3 = d * (playerRating3 - npcRating3 + 50) + bribeMod; 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; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); int roll = Misc::Rng::roll0to99(prng); if (type == PT_Admire) { target1 = std::max(iPerMinChance, target1); success = (roll <= target1); float c = floor(fPerDieRollMult * (target1 - roll)); x = success ? std::max(iPerMinChange, c) : c; } else if (type == PT_Intimidate) { target2 = std::max(iPerMinChance, target2); success = (roll <= target2); float r; if (roll != target2) r = floor(target2 - roll); else r = 1; if (roll <= target2) { float s = floor(r * fPerDieRollMult * fPerTempMult); 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)); if (success) { if (std::abs(c) < iPerMinChange) { // Deviating from Morrowind here: it doesn't increase disposition on marginal wins, // which seems to be a bug (MCP fixes it too). // Original logic: x = 0, y = -iPerMinChange x = iPerMinChange; y = x; // This goes unused. } else { x = -floor(c * fPerTempMult); y = c; } } else { x = floor(c * fPerTempMult); y = c; } } else if (type == PT_Taunt) { target1 = std::max(iPerMinChance, target1); success = (roll <= target1); float c = std::abs(floor(target1 - roll)); if (success) { float s = c * fPerDieRollMult * fPerTempMult; 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); if (success && std::abs(x) < iPerMinChange) x = -iPerMinChange; } else // Bribe { target3 = std::max(iPerMinChance, target3); success = (roll <= target3); float c = floor((target3 - roll) * fPerDieRollMult); x = success ? std::max(iPerMinChange, c) : c; } if (type == PT_Intimidate) { 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) { if (ptr.getClass().isActor()) mActors.forceStateUpdate(ptr); } 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, scripted); else 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()) mActors.skipAnimation(ptr); else mObjects.skipAnimation(ptr); } bool MechanicsManager::checkAnimationPlaying(const MWWorld::Ptr& ptr, std::string_view groupName) { 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()) return true; else return mObjects.onOpen(ptr); } void MechanicsManager::onClose(const MWWorld::Ptr& ptr) { if (!ptr.getClass().isActor()) mObjects.onClose(ptr); } void MechanicsManager::persistAnimationStates() { mActors.persistAnimationStates(); mObjects.persistAnimationStates(); } 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); } bool MechanicsManager::toggleAI() { mAI = !mAI; return mAI; } bool MechanicsManager::isAIActive() { return mAI; } void MechanicsManager::playerLoaded() { mUpdatePlayer = true; mClassSelected = true; mRaceSelected = true; mAI = true; } namespace { std::set makeBoundItemIdCache() { std::set boundItemIDCache; // Build a list of known bound item ID's const MWWorld::Store& gameSettings = MWBase::Environment::get().getESMStore()->get(); for (const ESM::GameSetting& currentSetting : gameSettings) { // Don't bother checking this GMST if it's not a sMagicBound* one. 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(ESM::RefId::stringRefId(currentGMSTValue)); } return boundItemIDCache; } } bool MechanicsManager::isBoundItem(const MWWorld::Ptr& item) { static const std::set boundItemIdCache = makeBoundItemIdCache(); return boundItemIdCache.find(item.getCellRef().getRefId()) != boundItemIdCache.end(); } 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 if (target.getClass().isDoor() && !cellref.isLocked() && cellref.getTrap().empty()) { return true; } if (!target.getClass().hasToolTip(target)) return true; // TODO: implement a better check to check if target is owned bed if (target.getClass().isActivator() && !target.getClass().getScript(target).startsWith("Bed")) return true; if (target.getClass().isNpc()) { if (target.getClass().getCreatureStats(target).isDead()) return true; if (target.getClass().getCreatureStats(target).getAiSequence().isInCombat()) return true; // check if a player tries to pickpocket a target NPC if (target.getClass().getCreatureStats(target).getKnockedDown() || isSneaking(ptr)) return false; return true; } if (isOwned(ptr, target, victim)) return false; // 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) { if (ptr.getClass().getNpcStats(ptr).isWerewolf()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sWerewolfRefusal}"); return true; } if (MWBase::Environment::get().getWorld()->getPlayer().enemiesNearby()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage2}"); return true; } MWWorld::Ptr victim; if (isAllowedToUse(ptr, bed, victim)) return false; if (commitCrime(ptr, victim, OT_SleepingInOwnedBed, bed.getCellRef().getFaction())) { MWBase::Environment::get().getWindowManager()->messageBox("#{sNotifyMessage64}"); return true; } else return false; } void MechanicsManager::unlockAttempted(const MWWorld::Ptr& ptr, const MWWorld::Ptr& item) { MWWorld::Ptr victim; 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 ESM::RefId& itemid) { std::vector> result; StolenItemsMap::const_iterator it = mStolenItems.find(itemid); if (it == mStolenItems.end()) return result; else { const OwnerMap& owners = it->second; for (OwnerMap::const_iterator ownerIt = owners.begin(); ownerIt != owners.end(); ++ownerIt) result.emplace_back(ownerIt->first.first, ownerIt->second); return result; } } bool MechanicsManager::isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) { StolenItemsMap::const_iterator it = mStolenItems.find(itemid); if (it == mStolenItems.end()) return false; const OwnerMap& owners = it->second; 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 ESM::RefId& factionid = ptr.getClass().getPrimaryFaction(ptr); if (!factionid.empty()) { 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) { if (player != getPlayer()) return; const ESM::RefId& itemId = item.getCellRef().getRefId(); StolenItemsMap::iterator stolenIt = mStolenItems.find(itemId); if (stolenIt == mStolenItems.end()) return; Owner owner; owner.first = victim.getCellRef().getRefId(); owner.second = false; 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; } // decrease count of stolen items int toRemove = std::min(count, mStolenItems[itemId][owner]); mStolenItems[itemId][owner] -= toRemove; if (mStolenItems[itemId][owner] == 0) { // erase owner from stolen items owners OwnerMap& owners = stolenIt->second; OwnerMap::iterator ownersIt = owners.find(owner); if (ownersIt != owners.end()) owners.erase(ownersIt); } MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); // move items from player to owner and report about theft 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) { 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(it->getCellRef().getRefId()); if (stolenIt == mStolenItems.end()) continue; OwnerMap& owners = stolenIt->second; int itemCount = it->getCellRef().getCount(); for (OwnerMap::iterator ownerIt = owners.begin(); ownerIt != owners.end();) { int toRemove = std::min(itemCount, ownerIt->second); itemCount -= toRemove; ownerIt->second -= toRemove; if (ownerIt->second == 0) owners.erase(ownerIt++); else ++ownerIt; } int toMove = it->getCellRef().getCount() - itemCount; 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) { if (ptr != getPlayer()) return; MWWorld::Ptr victim; bool isAllowed = true; const MWWorld::CellRef* ownerCellRef = &item.getCellRef(); if (!container.isEmpty()) { // Inherit the owner of the container ownerCellRef = &container.getCellRef(); isAllowed = isAllowedToUse(ptr, container, victim); } else { isAllowed = isAllowedToUse(ptr, item, victim); } if (isAllowed) return; Owner owner; owner.second = false; if (!container.isEmpty() && container.getClass().isActor()) { // "container" is an actor inventory, so just take actor's ID owner.first = ownerCellRef->getRefId(); } else { owner.first = ownerCellRef->getOwner(); if (owner.first.empty()) { owner.first = ownerCellRef->getFaction(); owner.second = true; } } const bool isGold = item.getClass().isGold(item); if (!isGold) { if (victim.isEmpty() || (victim.getClass().isActor() && victim.getCellRef().getCount() > 0 && !victim.getClass().getCreatureStats(victim).isDead())) mStolenItems[item.getCellRef().getRefId()][owner] += count; } if (alarm) { 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 ESM::RefId& factionId, int arg, bool victimAware) { // NOTE: victim may be empty // Only player can commit crime 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().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) neighbors.push_back(victim); // get the player's followers / allies (works recursively) that will not report crimes std::set playerFollowers; getActorsSidingWith(player, playerFollowers); // Did anyone see it? bool crimeSeen = false; 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))) { // 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, ESM::RefId::stringRefId("thief")); crimeSeen = true; } } if (crimeSeen) reportCrime(player, victim, type, factionId, arg); else if (type == OT_Assault && !victim.isEmpty()) { bool reported = false; if (victim.getClass().isClass(victim, "guard") && !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) 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, &playerFollowers); } return crimeSeen; } 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()) return false; if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(victim)) return false; // Unconsious actor can not report about crime and should not become hostile if (actor.getClass().getCreatureStats(actor).getKnockedDown()) return false; // Player's followers should not attack player, or try to arrest him if (actor.getClass().getCreatureStats(actor).getAiSequence().hasPackage(AiPackageTypeId::Follow)) { if (playerFollowers.find(actor) != playerFollowers.end()) return false; } return true; } 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().getESMStore()->get(); if (type == OT_Murder && !victim.isEmpty()) victim.getClass().getCreatureStats(victim).notifyMurder(); // Bounty and disposition penalty for each type of crime float disp = 0.f, dispVictim = 0.f; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) { arg = store.find("iCrimeTresspass")->mValue.getInteger(); disp = dispVictim = store.find("iDispTresspass")->mValue.getFloat(); } else if (type == OT_Pickpocket) { arg = store.find("iCrimePickPocket")->mValue.getInteger(); disp = dispVictim = store.find("fDispPickPocketMod")->mValue.getFloat(); } else if (type == OT_Assault) { arg = store.find("iCrimeAttack")->mValue.getInteger(); disp = store.find("iDispAttackMod")->mValue.getFloat(); dispVictim = store.find("fDispAttacking")->mValue.getFloat(); } else if (type == OT_Murder) { arg = store.find("iCrimeKilling")->mValue.getInteger(); disp = dispVictim = store.find("iDispKilling")->mValue.getFloat(); } else if (type == OT_Theft) { disp = dispVictim = store.find("fDispStealing")->mValue.getFloat() * arg; arg = static_cast(arg * store.find("fCrimeStealing")->mValue.getFloat()); arg = std::max(1, arg); // Minimum bounty of 1, in case items with zero value are stolen } // Make surrounding actors within alarm distance respond to the crime std::vector neighbors; const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); 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) neighbors.push_back(victim); int id = MWBase::Environment::get().getWorld()->getPlayer().getNewCrimeId(); // What amount of provocation did this crime generate? // Controls whether witnesses will engage combat with the criminal. int fight = 0, fightVictim = 0; if (type == OT_Trespassing || type == OT_SleepingInOwnedBed) fight = fightVictim = esmStore.get().find("iFightTrespass")->mValue.getInteger(); 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 } else if (type == OT_Assault) { fight = esmStore.get().find("iFightAttacking")->mValue.getInteger(); fightVictim = esmStore.get().find("iFightAttack")->mValue.getInteger(); } else if (type == OT_Murder) fight = fightVictim = esmStore.get().find("iFightKilling")->mValue.getInteger(); else if (type == OT_Theft) fight = fightVictim = esmStore.get().find("fFightStealing")->mValue.getInteger(); bool reported = false; std::set playerFollowers; getActorsSidingWith(player, playerFollowers); // Tell everyone (including the original reporter) in alarm range for (const MWWorld::Ptr& actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; // Will the witness report the crime? if (actor.getClass().getCreatureStats(actor).getAiSetting(AiSetting::Alarm).getBase() >= 100) { reported = true; if (type == OT_Trespassing) MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("intruder")); } } for (const MWWorld::Ptr& actor : neighbors) { if (!canReportCrime(actor, victim, playerFollowers)) continue; 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 observerStats.setAlarmed(true); setCrimeId = true; if (!observerStats.getAiSequence().isInPursuit()) { observerStats.getAiSequence().stack(AiPursue(player), actor); } } else { // 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; float fightTerm = static_cast(isActorVictim ? fightVictim : fight); fightTerm += getFightDispositionBias(dispTerm); fightTerm += getFightDistanceBias(actor, player); fightTerm *= alarmTerm; 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) { if (dispositionModifier != 0 && applyOnlyIfHostile) { dispositionModifier = std::clamp(dispositionModifier, -currentDisposition, 100 - currentDisposition); observerStats.modCrimeDispositionModifier(dispositionModifier); } startCombat(actor, player, &playerFollowers); // Apply aggression value to the base Fight rating, so that the actor can continue fighting // after a Calm spell wears off observerStats.setAiSetting(AiSetting::Fight, observerFightRating + static_cast(fightTerm)); 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( 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()) { const ESM::RefId& factionID = victim.getClass().getPrimaryFaction(victim); const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(factionID) != playerRanks.end()) { player.getClass().getNpcStats(player).expell(factionID, true); } } else if (!factionId.empty()) { const std::map& playerRanks = player.getClass().getNpcStats(player).getFactionRanks(); if (playerRanks.find(factionId) != playerRanks.end()) { player.getClass().getNpcStats(player).expell(factionId, true); } } if (type == OT_Assault && !victim.isEmpty() && !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().isInPursuit()) startCombat(victim, player, &playerFollowers); // Set the crime ID, which we will use to calm down participants // once the bounty has been paid. victim.getClass().getNpcStats(victim).setCrimeId(id); } } return reported; } bool MechanicsManager::actorAttacked(const MWWorld::Ptr& target, const MWWorld::Ptr& attacker) { const MWWorld::Ptr& player = getPlayer(); if (target == player || !attacker.getClass().isActor()) 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() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target) || attacker == player) && !seq.isInCombat(attacker)) { // 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().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; const ESM::RefId& script = target.getClass().getScript(target); if (!script.empty() && target.getRefData().getLocals().hasVar(script, "onpchitme") && attacker == player) { const int fight = target.getClass().getCreatureStats(target).getAiSetting(AiSetting::Fight).getModified(); peaceful = (fight == 0); } if (!peaceful) { 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) { 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) { if (attacker.isEmpty() || victim.isEmpty()) return; if (victim == attacker) return; // known to happen if (!victim.getClass().isNpc()) return; // TODO: implement animal rights const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); 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 if (attacker != player) { std::set playerFollowers; getActorsSidingWith(player, playerFollowers); if (playerFollowers.find(attacker) == playerFollowers.end()) return; } if (!canCommit && victimStats.getCrimeId() == -1) return; // Simple check for who attacked first: if the player attacked first, a crimeId should be set // Doesn't handle possible edge case where no one reported the assault, but in such a case, // for bystanders it is not possible to tell who attacked first, anyway. commitCrime(player, victim, MWBase::MechanicsManager::OT_Murder); } 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().getESMStore()->get(); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); float sneakTerm = 0; if (isSneaking(ptr)) { static float fSneakSkillMult = store.find("fSneakSkillMult")->mValue.getFloat(); static float fSneakBootMult = store.find("fSneakBootMult")->mValue.getFloat(); float sneak = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Sneak)); float agility = stats.getAttribute(ESM::Attribute::Agility).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float bootWeight = 0; if (ptr.getClass().isNpc() && MWBase::Environment::get().getWorld()->isOnGround(ptr)) { const MWWorld::InventoryStore& inv = ptr.getClass().getInventoryStore(ptr); MWWorld::ConstContainerStoreIterator it = inv.getSlot(MWWorld::InventoryStore::Slot_Boots); if (it != inv.end()) bootWeight = it->getClass().getWeight(*it); } sneakTerm = fSneakSkillMult * sneak + 0.2f * agility + 0.1f * luck + bootWeight * fSneakBootMult; } 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()); float distTerm = fSneakDistBase + fSneakDistMult * (pos1 - pos2).length(); 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().getOrDefault(ESM::MagicEffect::Blind).getMagnitude(); float obsSneak = observer.getClass().getSkill(observer, ESM::Skill::Sneak); float obsTerm = obsSneak + 0.2f * obsAgility + 0.1f * obsLuck - obsBlind; // is ptr behind the observer? static float fSneakNoViewMult = store.find("fSneakNoViewMult")->mValue.getFloat(); static float fSneakViewMult = store.find("fSneakViewMult")->mValue.getFloat(); float y = 0; osg::Vec3f vec = pos1 - pos2; if (observer.getRefData().getBaseNode()) { 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)) y = obsTerm * observerStats.getFatigueTerm() * fSneakNoViewMult; else y = obsTerm * observerStats.getFatigueTerm() * fSneakViewMult; } float target = x - y; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); return (Misc::Rng::roll0to99(prng) >= target); } void MechanicsManager::startCombat( const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set* targetAllies) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); // Don't add duplicate packages nor add packages to dead actors. if (stats.isDead() || stats.getAiSequence().isInCombat(target)) return; // The target is somehow the same as the actor. Early-out. if (ptr == target) { // 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, 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 (const Actor& actor : mActors) { if (actor.getPtr().getClass().isClass(actor.getPtr(), "Guard")) { MWMechanics::AiSequence& aiSeq = actor.getPtr().getClass().getCreatureStats(actor.getPtr()).getAiSequence(); if (aiSeq.getTypeId() == MWMechanics::AiPackageTypeId::Pursue) { aiSeq.stopPursuit(); aiSeq.stack(MWMechanics::AiCombat(target), ptr); actor.getPtr() .getClass() .getCreatureStats(actor.getPtr()) .setHitAttemptActorId( target.getClass() .getCreatureStats(target) .getActorId()); // Stops guard from ending combat if player is unreachable } } } } } // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly 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) { mActors.getObjectsInRange(position, radius, objects); mObjects.getObjectsInRange(position, radius, 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) { return mActors.isAnyObjectInRange(position, radius); } std::vector MechanicsManager::getActorsSidingWith(const MWWorld::Ptr& actor) { return mActors.getActorsSidingWith(actor); } std::vector MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor) { return mActors.getActorsFollowing(actor); } std::vector MechanicsManager::getActorsFollowingIndices(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingIndices(actor); } std::map MechanicsManager::getActorsFollowingByIndex(const MWWorld::Ptr& actor) { return mActors.getActorsFollowingByIndex(actor); } std::vector MechanicsManager::getActorsFighting(const MWWorld::Ptr& actor) { return mActors.getActorsFighting(actor); } std::vector MechanicsManager::getEnemiesNearby(const MWWorld::Ptr& actor) { return mActors.getEnemiesNearby(actor); } void MechanicsManager::getActorsFollowing(const MWWorld::Ptr& actor, std::set& out) { mActors.getActorsFollowing(actor, 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 } void MechanicsManager::write(ESM::ESMWriter& writer, Loading::Listener& listener) const { mActors.write(writer, listener); ESM::StolenItems items; items.mStolenItems = mStolenItems; writer.startRecord(ESM::REC_STLN); items.write(writer); writer.endRecord(ESM::REC_STLN); } void MechanicsManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_STLN) { ESM::StolenItems items; items.load(reader); mStolenItems = items.mStolenItems; } else mActors.readRecord(reader, type); } void MechanicsManager::clear() { mActors.clear(); mStolenItems.clear(); mClassSelected = false; mRaceSelected = false; } 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() .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); 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(MWWorld::Globals::sPCKnownWerewolf))) { const ESM::GameSetting* iWerewolfFightMod = MWBase::Environment::get().getESMStore()->get().find("iWerewolfFightMod"); fight += iWerewolfFightMod->mValue.getInteger(); } } return (fight >= 100); } void MechanicsManager::resurrect(const MWWorld::Ptr& ptr) { CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); if (stats.isDead()) { stats.resurrect(); mActors.resurrect(ptr); } } bool MechanicsManager::isCastingSpell(const MWWorld::Ptr& ptr) const { return mActors.isCastingSpell(ptr); } bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr& ptr) const { return mActors.isReadyToBlock(ptr); } bool MechanicsManager::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { return mActors.isAttackingOrSpell(ptr); } void MechanicsManager::setWerewolf(const MWWorld::Ptr& actor, bool werewolf) { MWMechanics::NpcStats& npcStats = actor.getClass().getNpcStats(actor); // The actor does not have to change state if (npcStats.isWerewolf() == werewolf) return; MWWorld::Player* player = &MWBase::Environment::get().getWorld()->getPlayer(); // 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); npcStats.setWerewolf(werewolf); MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); if (werewolf) { inv.unequipAll(); inv.equip(MWWorld::InventoryStore::Slot_Robe, inv.ContainerStore::add(ESM::RefId::stringRefId("werewolfrobe"), 1)); } else { inv.unequipSlot(MWWorld::InventoryStore::Slot_Robe); inv.ContainerStore::remove(ESM::RefId::stringRefId("werewolfrobe"), 1); } 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); } windowManager->setWerewolfOverlay(werewolf); // Witnesses of the player's transformation will make them a globally known werewolf std::vector 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) { if (neighbor == actor || !neighbor.getClass().isNpc()) continue; if (MWBase::Environment::get().getWorld()->getLOS(neighbor, actor) && awarenessCheck(actor, neighbor)) { detected = true; if (neighbor.getClass() .getCreatureStats(neighbor) .getAiSetting(MWMechanics::AiSetting::Alarm) .getModified() > 0) { reported = true; break; } } } if (detected) { windowManager->messageBox("#{sWerewolfAlarmMessage}"); MWBase::Environment::get().getWorld()->setGlobalInt(MWWorld::Globals::sPCKnownWerewolf, 1); if (reported) { npcStats.setBounty( std::max(0, npcStats.getBounty() + gmst.find("iWereWolfBounty")->mValue.getInteger())); } } } } void MechanicsManager::applyWerewolfAcrobatics(const MWWorld::Ptr& actor) { 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) { mActors.cleanupSummonedCreature(caster.getClass().getCreatureStats(caster), creatureActorId); } void MechanicsManager::reportStats(unsigned int frameNumber, osg::Stats& stats) const { stats.setAttribute(frameNumber, "Mechanics Actors", mActors.size()); stats.setAttribute(frameNumber, "Mechanics Objects", mObjects.size()); } int MechanicsManager::getGreetingTimer(const MWWorld::Ptr& ptr) const { return mActors.getGreetingTimer(ptr); } float MechanicsManager::getAngleToPlayer(const MWWorld::Ptr& ptr) const { return mActors.getAngleToPlayer(ptr); } GreetingState MechanicsManager::getGreetingState(const MWWorld::Ptr& ptr) const { return mActors.getGreetingState(ptr); } bool MechanicsManager::isTurningToPlayer(const MWWorld::Ptr& ptr) const { return mActors.isTurningToPlayer(ptr); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp000066400000000000000000000272121503074453300256760ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_MECHANICSMANAGERIMP_H #define GAME_MWMECHANICS_MECHANICSMANAGERIMP_H #include #include "../mwbase/mechanicsmanager.hpp" #include "../mwworld/ptr.hpp" #include "actors.hpp" #include "npcstats.hpp" #include "objects.hpp" namespace MWSound { enum class MusicType; } namespace MWWorld { class CellStore; } 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, 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; 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 getHoursToRest() const override; ///< Calculate how many hours the player needs to rest in order to be fully healed int getBarterOffer(const MWWorld::Ptr& ptr, int basePrice, bool buying) override; ///< This is used by every service to determine the price of objects given the trading skills of the player and ///< NPC. int getDerivedDisposition(const MWWorld::Ptr& ptr, bool clamp = true) override; ///< Calculate the diposition of an NPC toward the player. int countDeaths(const ESM::RefId& id) const override; ///< Return the number of deaths for actors with the given ID. void getPersuasionDispositionChange( const MWWorld::Ptr& npc, PersuasionType type, bool& success, int& tempChange, int& permChange) override; ///< Perform a persuasion action on NPC /// Check if \a observer is potentially aware of \a ptr. Does not do a line of sight check! bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) 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; void stopCombat(const MWWorld::Ptr& ptr) override; /** * @note victim may be empty * @param arg Depends on \a type, e.g. for Theft, the value of the item that was stolen. * @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; /// 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; /// 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; void forceStateUpdate(const MWWorld::Ptr& ptr) 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, std::string_view groupName) override; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; void persistAnimationStates() override; void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) 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 getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& objects) override; void getActorsInRange(const osg::Vec3f& position, float radius, std::vector& objects) override; /// Check if there are actors in selected range bool isAnyActorInRange(const osg::Vec3f& position, float radius) 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; std::vector getActorsFighting(const MWWorld::Ptr& actor) override; std::vector getEnemiesNearby(const MWWorld::Ptr& actor) 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 toggleAI() override; bool isAIActive() override; void playerLoaded() override; bool onOpen(const MWWorld::Ptr& ptr) override; void onClose(const MWWorld::Ptr& ptr) override; int countSavedGameRecords() const override; void write(ESM::ESMWriter& writer, Loading::Listener& listener) const override; void readRecord(ESM::ESMReader& reader, uint32_t type) override; void clear() override; bool isAggressive(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; void resurrect(const MWWorld::Ptr& ptr) override; bool isCastingSpell(const MWWorld::Ptr& ptr) const 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 castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) override; void processChangedSettings(const Settings::CategorySettingVector& settings) override; void notifyDied(const MWWorld::Ptr& actor) 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 confiscateStolenItems(const MWWorld::Ptr& player, const MWWorld::Ptr& targetContainer) 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; /// Has the player stolen this item from the given owner? bool isItemStolenFrom(const ESM::RefId& itemid, const MWWorld::Ptr& ptr) override; bool isBoundItem(const MWWorld::Ptr& item) 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 setWerewolf(const MWWorld::Ptr& actor, bool werewolf) override; void applyWerewolfAcrobatics(const MWWorld::Ptr& actor) override; void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) override; void confiscateStolenItemToOwner( const MWWorld::Ptr& player, const MWWorld::Ptr& item, const MWWorld::Ptr& victim, int count) override; bool isAttackPreparing(const MWWorld::Ptr& ptr) override; bool isRunning(const MWWorld::Ptr& ptr) override; bool isSneaking(const MWWorld::Ptr& ptr) override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const 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); bool reportCrime(const MWWorld::Ptr& ptr, const MWWorld::Ptr& victim, OffenseType type, const ESM::RefId& factionId, int arg = 0); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/movement.hpp000066400000000000000000000020531503074453300235310ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_MOVEMENT_H #define GAME_MWMECHANICS_MOVEMENT_H #include namespace MWMechanics { /// Desired movement for an actor struct Movement { // Desired movement. Direction is relative to the current orientation. // Length of the vector controls desired speed. 0 - stay, 0.5 - half-speed, 1.0 - max speed. float mPosition[3]; // Desired rotation delta (euler angles). float mRotation[3]; // Controlled by CharacterController, should not be changed from other places. // These fields can not be private fields in CharacterController, because Actor::getCurrentSpeed uses it. float mSpeedFactor; bool mIsStrafing; Movement() { mPosition[0] = mPosition[1] = mPosition[2] = 0.0f; mRotation[0] = mRotation[1] = mRotation[2] = 0.0f; mSpeedFactor = 1.f; mIsStrafing = false; } osg::Vec3f asVec3() { return osg::Vec3f(mPosition[0], mPosition[1], mPosition[2]); } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/npcstats.cpp000066400000000000000000000345321503074453300235400ustar00rootroot00000000000000#include "npcstats.hpp" #include #include #include #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" MWMechanics::NpcStats::NpcStats() : 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) { mSpecIncreases.resize(3, 0); for (const ESM::Skill& skill : MWBase::Environment::get().getESMStore()->get()) mSkills.emplace(skill.mId, SkillValue{}); } int MWMechanics::NpcStats::getBaseDisposition() const { return mDisposition; } void MWMechanics::NpcStats::setBaseDisposition(int disposition) { mDisposition = disposition; } int MWMechanics::NpcStats::getCrimeDispositionModifier() const { return mCrimeDispositionModifier; } void MWMechanics::NpcStats::setCrimeDispositionModifier(int value) { mCrimeDispositionModifier = value; } void MWMechanics::NpcStats::modCrimeDispositionModifier(int value) { mCrimeDispositionModifier += value; } 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; } MWMechanics::SkillValue& MWMechanics::NpcStats::getSkill(ESM::RefId id) { auto it = mSkills.find(id); if (it == mSkills.end()) throw std::runtime_error("skill not found"); return it->second; } 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 { return mFactionRank; } int MWMechanics::NpcStats::getFactionRank(const ESM::RefId& faction) const { auto it = mFactionRank.find(faction); if (it != mFactionRank.end()) return it->second; return -1; } void MWMechanics::NpcStats::joinFaction(const ESM::RefId& faction) { auto it = mFactionRank.find(faction); if (it == mFactionRank.end()) mFactionRank[faction] = 0; } void MWMechanics::NpcStats::setFactionRank(const ESM::RefId& faction, int newRank) { auto it = mFactionRank.find(faction); if (it != mFactionRank.end()) { const ESM::Faction* factionPtr = MWBase::Environment::get().getESMStore()->get().find(faction); if (newRank < 0) { mFactionRank.erase(it); 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()); } } bool MWMechanics::NpcStats::getExpelled(const ESM::RefId& factionID) const { return mExpelled.find(factionID) != mExpelled.end(); } void MWMechanics::NpcStats::expell(const ESM::RefId& factionID, bool printMessage) { if (mExpelled.find(factionID) == mExpelled.end()) { mExpelled.insert(factionID); if (!printMessage) return; std::string message = "#{sExpelledMessage}"; message += MWBase::Environment::get().getESMStore()->get().find(factionID)->mName; MWBase::Environment::get().getWindowManager()->messageBox(message); } } void MWMechanics::NpcStats::clearExpelled(const ESM::RefId& factionID) { mExpelled.erase(factionID); } bool MWMechanics::NpcStats::isInFaction(const ESM::RefId& faction) const { return (mFactionRank.find(faction) != mFactionRank.end()); } int MWMechanics::NpcStats::getFactionReputation(const ESM::RefId& faction) const { auto iter = mFactionReputation.find(faction); if (iter == mFactionReputation.end()) return 0; return iter->second; } void MWMechanics::NpcStats::setFactionReputation(const ESM::RefId& faction, int value) { mFactionReputation[faction] = value; } float MWMechanics::NpcStats::getSkillProgressRequirement(ESM::RefId id, const ESM::Class& class_) const { float progressRequirement = 1.f + getSkill(id).getBase(); const MWWorld::Store& gmst = MWBase::Environment::get().getESMStore()->get(); const ESM::Skill* skill = MWBase::Environment::get().getESMStore()->get().find(id); float typeFactor = gmst.find("fMiscSkillBonus")->mValue.getFloat(); int index = ESM::Skill::refIdToIndex(skill->mId); for (const auto& skills : class_.mData.mSkills) { if (skills[0] == index) { typeFactor = gmst.find("fMinorSkillBonus")->mValue.getFloat(); break; } else if (skills[1] == index) { typeFactor = gmst.find("fMajorSkillBonus")->mValue.getFloat(); break; } } progressRequirement *= typeFactor; if (typeFactor <= 0) throw std::runtime_error("invalid skill type factor"); float specialisationFactor = 1; if (skill->mData.mSpecialization == class_.mData.mSpecialization) { specialisationFactor = gmst.find("fSpecialSkillBonus")->mValue.getFloat(); if (specialisationFactor <= 0) throw std::runtime_error("invalid skill specialisation factor"); } progressRequirement *= specialisationFactor; return progressRequirement; } int MWMechanics::NpcStats::getLevelProgress() const { return mLevelProgress; } void MWMechanics::NpcStats::setLevelProgress(int progress) { mLevelProgress = progress; } void MWMechanics::NpcStats::levelUp() { 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 mSkillIncreases.clear(); const float endurance = getAttribute(ESM::Attribute::Endurance).getBase(); // "When you gain a level, in addition to increasing three primary attributes, your Health // will automatically increase by 10% of your Endurance attribute. If you increased Endurance this level, // the Health increase is calculated from the increased Endurance" // Note: we should add bonus Health points to current level too. float healthGain = endurance * gmst.find("fLevelUpHealthEndMult")->mValue.getFloat(); MWMechanics::DynamicStat health(getHealth()); health.setBase(getHealth().getBase() + healthGain); health.setCurrent(std::max(1.f, getHealth().getCurrent() + healthGain)); setHealth(health); setLevel(getLevel() + 1); } void MWMechanics::NpcStats::updateHealth() { const float endurance = getAttribute(ESM::Attribute::Endurance).getBase(); const float strength = getAttribute(ESM::Attribute::Strength).getBase(); setHealth(floor(0.5f * (strength + endurance))); } int MWMechanics::NpcStats::getLevelupAttributeMultiplier(ESM::Attribute::AttributeID attribute) const { auto it = mSkillIncreases.find(attribute); if (it == mSkillIncreases.end() || it->second == 0) return 1; 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().getESMStore()->get().find(gmst.str())->mValue.getInteger(); } 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::setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases) { assert(spec >= 0 && spec < 3); mSpecIncreases[spec] = increases; } void MWMechanics::NpcStats::flagAsUsed(const ESM::RefId& id) { mUsedIds.insert(id); } bool MWMechanics::NpcStats::hasBeenUsed(const ESM::RefId& id) const { return mUsedIds.find(id) != mUsedIds.end(); } int MWMechanics::NpcStats::getBounty() const { return mBounty; } void MWMechanics::NpcStats::setBounty(int bounty) { mBounty = bounty; } int MWMechanics::NpcStats::getReputation() const { return mReputation; } void MWMechanics::NpcStats::setReputation(int reputation) { // Reputation is capped in original engine mReputation = std::clamp(reputation, 0, 255); } int MWMechanics::NpcStats::getCrimeId() const { return mCrimeId; } void MWMechanics::NpcStats::setCrimeId(int id) { mCrimeId = id; } bool MWMechanics::NpcStats::hasSkillsForRank(const ESM::RefId& factionId, int rank) const { const ESM::Faction& faction = *MWBase::Environment::get().getESMStore()->get().find(factionId); const ESM::RankData& rankData = faction.mData.mRankData.at(rank); std::vector skills; for (int index : faction.mData.mSkills) { 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::vector::const_reverse_iterator iter = skills.rbegin(); if (*iter < rankData.mPrimarySkill) return false; if (skills.size() < 2) return true; iter++; if (*iter < rankData.mFavouredSkill) return false; if (skills.size() < 3) return true; iter++; if (*iter < rankData.mFavouredSkill) return false; return true; } bool MWMechanics::NpcStats::isWerewolf() const { return mIsWerewolf; } void MWMechanics::NpcStats::setWerewolf(bool set) { if (mIsWerewolf == set) return; if (set != false) { mWerewolfKills = 0; } mIsWerewolf = set; } int MWMechanics::NpcStats::getWerewolfKills() const { return mWerewolfKills; } void MWMechanics::NpcStats::addWerewolfKill() { ++mWerewolfKills; } float MWMechanics::NpcStats::getTimeToStartDrowning() const { return mTimeToStartDrowning; } void MWMechanics::NpcStats::setTimeToStartDrowning(float time) { mTimeToStartDrowning = time; } void MWMechanics::NpcStats::writeState(ESM::CreatureStats& state) const { CreatureStats::writeState(state); } void MWMechanics::NpcStats::writeState(ESM::NpcStats& state) const { 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 (const auto& [id, value] : mSkills) { // TODO extend format auto index = ESM::Skill::refIdToIndex(id); assert(index >= 0); value.writeState(state.mSkills[static_cast(index)]); } state.mIsWerewolf = mIsWerewolf; state.mCrimeId = mCrimeId; state.mBounty = mBounty; for (auto iter(mExpelled.begin()); iter != mExpelled.end(); ++iter) state.mFactions[*iter].mExpelled = true; 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; state.mSkillIncrease.fill(0); for (const auto& [key, value] : mSkillIncreases) { // TODO extend format auto index = ESM::Attribute::refIdToIndex(key); assert(index >= 0); state.mSkillIncrease[static_cast(index)] = value; } 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)); state.mTimeToStartDrowning = mTimeToStartDrowning; } void MWMechanics::NpcStats::readState(const ESM::CreatureStats& state) { CreatureStats::readState(state); } void MWMechanics::NpcStats::readState(const ESM::NpcStats& state) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); 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); if (iter->second.mRank >= 0) mFactionRank[iter->first] = iter->second.mRank; if (iter->second.mReputation) mFactionReputation[iter->first] = iter->second.mReputation; } mDisposition = state.mDisposition; mCrimeDispositionModifier = state.mCrimeDispositionModifier; for (size_t i = 0; i < state.mSkills.size(); ++i) { // TODO extend format ESM::RefId id = ESM::Skill::indexToRefId(i); assert(!id.empty()); mSkills[id].readState(state.mSkills[i]); } mIsWerewolf = state.mIsWerewolf; mCrimeId = state.mCrimeId; mBounty = state.mBounty; mReputation = state.mReputation; mWerewolfKills = state.mWerewolfKills; mLevelProgress = state.mLevelProgress; for (size_t i = 0; i < state.mSkillIncrease.size(); ++i) mSkillIncreases[ESM::Attribute::indexToRefId(i)] = state.mSkillIncrease[i]; for (size_t i = 0; i < state.mSpecIncreases.size(); ++i) mSpecIncreases[i] = state.mSpecIncreases[i]; for (auto iter(state.mUsedIds.begin()); iter != state.mUsedIds.end(); ++iter) if (store.find(*iter)) mUsedIds.insert(*iter); mTimeToStartDrowning = state.mTimeToStartDrowning; } openmw-openmw-0.49.0/apps/openmw/mwmechanics/npcstats.hpp000066400000000000000000000116211503074453300235370ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_NPCSTATS_H #define GAME_MWMECHANICS_NPCSTATS_H #include "creaturestats.hpp" #include #include #include #include #include #include #include namespace ESM { struct Class; struct NpcStats; } namespace MWMechanics { /// \brief Additional stats for NPCs class NpcStats : public CreatureStats { int mDisposition; int mCrimeDispositionModifier; std::map mSkills; // SkillValue.mProgress used by the player only int mReputation; int mCrimeId; // ----- 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; // --------------------------------------------------------------------------- /// Countdown to getting damage while underwater float mTimeToStartDrowning; bool mIsWerewolf; public: NpcStats(); int getBaseDisposition() const; void setBaseDisposition(int disposition); int getCrimeDispositionModifier() const; void setCrimeDispositionModifier(int value); void modCrimeDispositionModifier(int value); int getReputation() const; void setReputation(int reputation); int getCrimeId() const; void setCrimeId(int id); const SkillValue& getSkill(ESM::RefId id) const; SkillValue& getSkill(ESM::RefId id); void setSkill(ESM::RefId id, const SkillValue& value); int getFactionRank(const ESM::RefId& faction) const; const std::map& getFactionRanks() const; /// 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); 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); bool isInFaction(const ESM::RefId& faction) const; float getSkillProgressRequirement(ESM::RefId id, const ESM::Class& class_) const; int getLevelProgress() const; void setLevelProgress(int progress); int getLevelupAttributeMultiplier(ESM::Attribute::AttributeID attribute) const; int getSkillIncreasesForAttribute(ESM::Attribute::AttributeID attribute) const; void setSkillIncreasesForAttribute(ESM::Attribute::AttributeID, int increases); int getSkillIncreasesForSpecialization(ESM::Class::Specialization spec) const; void setSkillIncreasesForSpecialization(ESM::Class::Specialization spec, int increases); void levelUp(); 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 bool hasBeenUsed(const ESM::RefId& id) const; ///< @note Id must be lower-case int getBounty() const; void setBounty(int bounty); int getFactionReputation(const ESM::RefId& faction) const; void setFactionReputation(const ESM::RefId& faction, int value); bool hasSkillsForRank(const ESM::RefId& factionId, int rank) const; bool isWerewolf() const; void setWerewolf(bool set); int getWerewolfKills() const; /// 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); void writeState(ESM::CreatureStats& state) const; void writeState(ESM::NpcStats& state) const; void readState(const ESM::CreatureStats& state); void readState(const ESM::NpcStats& state); const std::map& getSkills() const { return mSkills; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/objects.cpp000066400000000000000000000114601503074453300233250ustar00rootroot00000000000000#include "objects.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "character.hpp" namespace MWMechanics { void Objects::addObject(const MWWorld::Ptr& ptr) { removeObject(ptr); MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (anim == nullptr) return; const auto it = mObjects.emplace(mObjects.end(), ptr, anim); mIndex.emplace(ptr.mRef, it); } void Objects::removeObject(const MWWorld::Ptr& ptr) { 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) { const auto iter = mIndex.find(old.mRef); if (iter != mIndex.end()) iter->second->updatePtr(ptr); } 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::update(float duration, bool paused) { if (!paused) { for (CharacterController& object : mObjects) object.update(duration); } else { // 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()); } } } } bool Objects::onOpen(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) return iter->second->onOpen(); return true; } void Objects::onClose(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) iter->second->onClose(); } 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()) { 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::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::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) iter->second->enableLuaAnimations(enable); } void Objects::skipAnimation(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); if (iter != mIndex.end()) iter->second->skipAnim(); } void Objects::persistAnimationStates() { for (CharacterController& object : mObjects) object.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) const { for (const CharacterController& object : mObjects) if ((position - object.getPtr().getRefData().getPosition().asVec3()).length2() <= radius * radius) out.push_back(object.getPtr()); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/objects.hpp000066400000000000000000000035071503074453300233350ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_ACTIVATORS_H #define GAME_MWMECHANICS_ACTIVATORS_H #include "character.hpp" #include #include #include #include namespace osg { class Vec3f; } namespace MWWorld { class Ptr; class CellStore; } namespace MWMechanics { class Objects { std::list mObjects; std::map::iterator> mIndex; public: void addObject(const MWWorld::Ptr& ptr); ///< Register an animated object void removeObject(const MWWorld::Ptr& ptr); ///< Deregister an object void updateObject(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr); ///< Updates an object with a new Ptr void dropObjects(const MWWorld::CellStore* cellStore); ///< Deregister all objects in the given cell. void update(float duration, bool paused); ///< Update object animations bool onOpen(const MWWorld::Ptr& ptr); void onClose(const MWWorld::Ptr& ptr); 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) const; std::size_t size() const { return mObjects.size(); } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/obstacle.cpp000066400000000000000000000204561503074453300234750ustar00rootroot00000000000000#include "obstacle.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "movement.hpp" namespace MWMechanics { namespace { // 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()) return false; else return true; } struct GetNearbyDoorVisitor { MWWorld::Ptr mResult; GetNearbyDoorVisitor(const MWWorld::Ptr& actor, const float minDist) : mPos(actor.getRefData().getPosition().asVec3()) , mDir(actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0, 1, 0)) , mMinDist(minDist) { mPos.z() = 0; mDir.normalize(); } bool operator()(const MWWorld::Ptr& ptr) { MWWorld::LiveCellRef& ref = *static_cast*>(ptr.getBase()); if (!ptr.getRefData().isEnabled() || ref.isDeleted()) return true; if (ptr.getClass().getDoorState(ptr) != MWWorld::DoorState::Idle) return true; const float doorRot = ref.mData.getPosition().rot[2] - ptr.getCellRef().getPosition().rot[2]; if (doorRot != 0) return true; osg::Vec3f doorPos(ref.mData.getPosition().asVec3()); doorPos.z() = 0; osg::Vec3f actorToDoor = doorPos - mPos; // Door is not close enough if (actorToDoor.length2() > mMinDist * mMinDist) return true; actorToDoor.normalize(); const float angle = std::acos(mDir * actorToDoor); // Allow 60 degrees angle between actor and door if (angle < -osg::PI / 3 || angle > osg::PI / 3) return true; mResult = ptr; return false; // found, stop searching } private: osg::Vec3f mPos, mDir; float mMinDist; }; const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist) { GetNearbyDoorVisitor visitor(actor, minDist); actor.getCell()->forEachType(visitor); return visitor.mResult; } bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer, std::vector* occupyingActors) { const auto world = MWBase::Environment::get().getWorld(); const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents; const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z())); if (ignorePlayer) { const std::array ignore{ actor, world->getPlayerConstPtr() }; return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); } const std::array ignore{ actor }; return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors); } ObstacleCheck::ObstacleCheck() : mEvadeDirectionIndex(std::size(evadeDirections) - 1) { } void ObstacleCheck::clear() { mWalkState = WalkState::Initial; } bool ObstacleCheck::isEvading() const { return mWalkState == WalkState::Evade; } /* * input - actor, duration (time since last check) * output - true if evasive action needs to be taken * * Walking state transitions (player greeting check not shown): * * Initial ----> Norm <--------> CheckStuck -------> Evade ---+ * ^ ^ | f ^ | t ^ | | * | | | | | | | | * | +-+ +---+ +---+ | u * | any < t < u | * +---------------------------------------------+ * * f = one reaction time * t = how long before considered stuck * u = how long to move sideways * */ void ObstacleCheck::update(const MWWorld::Ptr& actor, const osg::Vec3f& destination, float duration, MWWorld::MovementDirectionFlags supportedMovementDirection) { const auto position = actor.getRefData().getPosition().asVec3(); if (mWalkState == WalkState::Initial) { mWalkState = WalkState::Norm; mStateDuration = 0; mPrev = position; mInitialDistance = (destination - position).length(); mDestination = destination; return; } if (mWalkState != WalkState::Evade) { 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; const float movedFromInitialDistance = mInitialDistance - currentDistance; mPrev = position; if (movedDistance >= distSameSpot && movedFromInitialDistance >= distSameSpot) { mWalkState = WalkState::Norm; mStateDuration = 0; return; } if (mWalkState == WalkState::Norm) { mWalkState = WalkState::CheckStuck; mStateDuration = duration; mInitialDistance = (destination - position).length(); return; } mStateDuration += duration; if (mStateDuration < durationSameSpot) { return; } mWalkState = WalkState::Evade; mStateDuration = 0; 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 >= durationToEvade) { // tried to evade, assume all is ok and start again mWalkState = WalkState::Norm; mStateDuration = 0; mPrev = position; } } void ObstacleCheck::takeEvasiveAction(MWMechanics::Movement& actorMovement) const { actorMovement.mPosition[0] = evadeDirections[mEvadeDirectionIndex].mMovementX; actorMovement.mPosition[1] = evadeDirections[mEvadeDirectionIndex].mMovementY; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/obstacle.hpp000066400000000000000000000033341503074453300234760ustar00rootroot00000000000000#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; /// tests actor's proximity to a closed door by default bool proximityToDoor(const MWWorld::Ptr& actor, float minDist); /// Returns door pointer within range. No guarantee is given as to which one /** \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, 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/pathfinding.cpp000066400000000000000000000502131503074453300241660ustar00rootroot00000000000000#include "pathfinding.hpp" #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwphysics/raycasting.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.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) { assert(grid && !grid->mPoints.empty()); float closestDistanceBetween = std::numeric_limits::max(); float closestDistanceReachable = std::numeric_limits::max(); int closestIndex = 0; int closestReachableIndex = 0; // TODO: if this full scan causes performance problems mapping pathgrid // points to a quadtree may help for (size_t counter = 0; counter < grid->mPoints.size(); counter++) { float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < closestDistanceReachable) { // found a closer one if (graph->isPointConnected(start, counter)) { closestDistanceReachable = potentialDistBetween; closestReachableIndex = counter; } if (potentialDistBetween < closestDistanceBetween) { closestDistanceBetween = potentialDistBetween; closestIndex = counter; } } } // post-condition: start and endpoint must be connected assert(graph->isPointConnected(start, closestReachableIndex)); // AiWander has logic that depends on whether a path was created, deleting // 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); } float sqrDistance(const osg::Vec2f& lhs, const osg::Vec2f& rhs) { return (lhs - rhs).length2(); } float sqrDistanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs) { return sqrDistance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); } float getHeight(const MWWorld::ConstPtr& actor) { const auto world = MWBase::Environment::get().getWorld(); const auto halfExtents = world->getHalfExtents(actor); return 2.0 * halfExtents.z(); } // 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) { osg::Vec3f v1 = p1 - p2; osg::Vec3f v3 = p3 - p2; v1.z() = v3.z() = 0; float dotProduct = v1.x() * v3.x() + v1.y() * v3.y(); float crossProduct = v1.x() * v3.y() - v1.y() * v3.x(); // Check that the angle between v1 and v3 is less or equal than 5 degrees. static const float cos175 = std::cos(osg::PI * (175.0 / 180)); bool checkAngle = dotProduct <= cos175 * v1.length() * v3.length(); // Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`. bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length(); 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 { float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs) { if (std::abs(lhs.z() - rhs.z()) > getHeight(actor) || canActorMoveByZAxis(actor)) return distance(lhs, rhs); return distanceIgnoreZ(lhs, rhs); } bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY) { osg::Vec3f dir = to - from; 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; // 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); return (std::abs(from.z() - h) <= PATHFIND_Z_REACH); } /* * NOTE: This method may fail to find a path. The caller must check the * result before using it. If there is no path the AI routies need to * implement some other heuristics to reach the target. * * NOTE: It may be desirable to simply go directly to the endPoint if for * example there are no pathgrids in this cell. * * NOTE: startPoint & endPoint are in world coordinates * * Updates mPath using aStarSearch() or ray test (if shortcut allowed). * mPath consists of pathgrid points, except the last element which is * endPoint. This may be useful where the endPoint is not on a pathgrid * point (e.g. combat). However, if the caller has already chosen a * pathgrid point (e.g. wander) then it may be worth while to call * pop_back() to remove the redundant entry. * * NOTE: coordinates must be converted prior to calling getClosestPoint() * * | * | cell * | +-----------+ * | | | * | | | * | | @ | * | i | j | * |<--->|<---->| | * | +-----------+ * | k * |<---------->| world * +----------------------------- * * i = x value of cell itself (multiply by ESM::Land::REAL_SIZE to convert) * j = @.x in local coordinates (i.e. within the cell) * k = @.x in world coordinates */ void PathFinder::buildPathByPathgridImpl(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, std::back_insert_iterator> out) { const auto pathgrid = pathgridGraph.getPathgrid(); // 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()) return; // NOTE: getClosestPoint expects local coordinates 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 // outside an area enclosed by walls, but there is a pathgrid // point right behind the wall that is closer than any pathgrid // point outside the wall osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); int startNode = getClosestPoint(pathgrid, startPointInLocalCoords); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); std::pair endNode = getClosestReachablePoint(pathgrid, &pathgridGraph, endPointInLocalCoords, startNode); // if it's shorter for actor to travel from start to end, than to travel from either // start or end to nearest pathgrid point, just travel from start to end. float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) { *out++ = endPoint; return; } // AiWander has logic that depends on whether a path was created, // deleting allowed nodes if not. Hence a path needs to be created // 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) { ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); converter.toWorld(temp); *out++ = makeOsgVec3(temp); } else { auto path = pathgridGraph.aStarSearch(startNode, endNode.first); // If nearest path node is in opposite direction from second, remove it from path. // Especially useful for wandering actors, if the nearest node is blocked for some reason. if (path.size() > 1) { ESM::Pathgrid::Point secondNode = *(++path.begin()); osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; if (toSecondNodeVec3f * toStartPointVec3f > 0) { ESM::Pathgrid::Point temp(secondNode); converter.toWorld(temp); // 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() ->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); }); } // If endNode found is NOT the closest PathGrid point to the endPoint, // assume endPoint is not reachable from endNode. In which case, // path ends at endNode. // // So only add the destination (which may be different to the closest // pathgrid point) when endNode was the closest point to endPoint. // // This logic can fail in the opposite situate, e.g. endPoint may // have been reachable but happened to be very close to an // unreachable pathgrid point. // // The AI routines will have to deal with such situations. 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()) return 0.; const auto& nextPoint = mPath.front(); const float directionX = nextPoint.x() - x; const float directionY = nextPoint.y() - y; return std::atan2(directionX, directionY); } float PathFinder::getXAngleToNext(float x, float y, float z) const { // This should never happen (programmers should have an if statement checking // isPathConstructed that prevents this call if otherwise). if (mPath.empty()) return 0.; const osg::Vec3f dir = mPath.front() - osg::Vec3f(x, y, z); return getXAngleToDir(dir); } void PathFinder::update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, UpdateFlags updateFlags, const DetourNavigator::AgentBounds& agentBounds, DetourNavigator::Flags pathFlags) { if (mPath.empty()) return; while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); 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) && isValidShortcut(mPath[0], mPath[2])) mPath.erase(mPath.begin() + 1); 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 ((updateFlags & UpdateFlag_CanMoveByZ) != 0) distSqr = (mPath.front() - position).length2(); else distSqr = sqrDistanceIgnoreZ(mPath.front(), position); if (distSqr < destinationTolerance * destinationTolerance) mPath.pop_front(); } } void PathFinder::buildStraightPath(const osg::Vec3f& endPoint) { mPath.clear(); mPath.push_back(endPoint); mConstructed = true; } void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph) { mPath.clear(); mCell = cell; buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath)); mConstructed = !mPath.empty(); } void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, pathType, std::back_inserter(mPath)); 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 DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) { mPath.clear(); mCell = cell; DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) { status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, pathType, std::back_inserter(mPath)); if (status != DetourNavigator::Status::Success) mPath.clear(); } 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 (status == DetourNavigator::Status::NavMeshNotFound && mPath.empty()) mPath.push_back(endPoint); mConstructed = !mPath.empty(); } 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 navigator = world->getNavigator(); const auto status = DetourNavigator::findPath( *navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); 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 " << endPoint << " with flags (" << DetourNavigator::WriteFlags{ flags } << ")"; } 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 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 startToEnd = endPoint - startPoint; const auto distance = startToEnd.length(); if (distance <= maxDistance) 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, agentBounds, flags, areaCosts, endTolerance, pathType); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/pathfinding.hpp000066400000000000000000000176741503074453300242110ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_PATHFINDING_H #define GAME_MWMECHANICS_PATHFINDING_H #include #include #include #include #include #include #include #include namespace MWWorld { class CellStore; class ConstPtr; class Ptr; } namespace DetourNavigator { struct AgentBounds; } namespace MWMechanics { class PathgridGraph; 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"); return (lhs - rhs).length(); } inline float distanceIgnoreZ(const osg::Vec3f& lhs, const osg::Vec3f& rhs) { return distance(osg::Vec2f(lhs.x(), lhs.y()), osg::Vec2f(rhs.x(), rhs.y())); } float getPathDistance(const MWWorld::Ptr& actor, const osg::Vec3f& lhs, const osg::Vec3f& rhs); inline float getZAngleToDir(const osg::Vec3f& dir) { return std::atan2(dir.x(), dir.y()); } inline float getXAngleToDir(const osg::Vec3f& dir) { float dirLen = dir.length(); return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0; } inline float getZAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest) { return getZAngleToDir(dest - origin); } inline float getXAngleToPoint(const osg::Vec3f& origin, const osg::Vec3f& dest) { return getXAngleToDir(dest - origin); } const float PATHFIND_Z_REACH = 50.0f; // distance after which actor (failed previously to shortcut) will try again const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f; const float MIN_TOLERANCE = 1.0f; const float DEFAULT_TOLERANCE = 32.0f; // cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target; // magnitude of pits/obstacles is defined by PATHFIND_Z_REACH bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY); enum class PathType { Full, Partial, }; class PathFinder { public: 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++) { float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos); if (potentialDistBetween < distanceBetween) { distanceBetween = potentialDistBetween; closestIndex = counter; } } return closestIndex; } 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); [[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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/pathgrid.cpp000066400000000000000000000264071503074453300235050ustar00rootroot00000000000000#include "pathgrid.hpp" #include #include #include namespace { // See https://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html // // One of the smallest cost in Seyda Neen is between points 77 & 78: // pt x y // 77 = 8026, 4480 // 78 = 7986, 4218 // // Euclidean distance is about 262 (ignoring z) and Manhattan distance is 300 // (again ignoring z). Using a value of about 300 for D seems like a reasonable // starting point for experiments. If in doubt, just use value 1. // // The distance between 3 & 4 are pretty small, too. // 3 = 5435, 223 // 4 = 5948, 193 // // Approx. 514 Euclidean distance and 533 Manhattan distance. // float manhattan(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b) { return 300.0f * (abs(a.mX - b.mX) + abs(a.mY - b.mY) + abs(a.mZ - b.mZ)); } // Choose a heuristics - Note that these may not be the best for directed // graphs with non-uniform edge costs. // // distance: // - sqrt((curr.x - goal.x)^2 + (curr.y - goal.y)^2 + (curr.z - goal.z)^2) // - slower but more accurate // // Manhattan: // - |curr.x - goal.x| + |curr.y - goal.y| + |curr.z - goal.z| // - faster but not the shortest path float costAStar(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b) { // return distance(a, b); return manhattan(a, b); } constexpr size_t NoIndex = static_cast(-1); } namespace MWMechanics { class PathgridGraph::Builder { 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. * * The data structure is based on the code in buildPath2() but modified. * Please check git history if interested. * * mGraph[v].edges[i].index = w * * v = point index of location "from" * i = index of edges from point v * w = point index of location "to" * * * Example: (notice from p(0) to p(2) is not allowed in this example) * * mGraph[0].edges[0].index = 1 * .edges[1].index = 3 * * mGraph[1].edges[0].index = 0 * .edges[1].index = 2 * .edges[2].index = 3 * * mGraph[2].edges[0].index = 1 * * (etc, etc) * * * low * cost * p(0) <---> p(1) <------------> p(2) * ^ ^ * | | * | +-----> p(3) * +----------------> * high cost */ PathgridGraph::PathgridGraph(const ESM::Pathgrid& pathgrid) : mPathgrid(&pathgrid) { mGraph.resize(mPathgrid->mPoints.size()); for (const auto& edge : mPathgrid->mEdges) { ConnectedPoint neighbour; neighbour.cost = costAStar(mPathgrid->mPoints[edge.mV0], mPathgrid->mPoints[edge.mV1]); // forward path of the edge 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 = edge.mV0; // mGraph[edge.mV1].edges.push_back(neighbour); } Builder(*this); } const PathgridGraph PathgridGraph::sEmpty = {}; bool PathgridGraph::isPointConnected(const size_t start, const size_t end) const { return (mGraph[start].componentId == mGraph[end].componentId); } void PathgridGraph::getNeighbouringPoints(const size_t index, ESM::Pathgrid::PointList& nodes) const { for (const auto& edge : mGraph[index].edges) { if (edge.index != index) nodes.push_back(mPathgrid->mPoints[edge.index]); } } /* * NOTE: Based on buildPath2(), please check git history if interested * Should consider using a 3rd party library version (e.g. boost) * * Find the shortest path to the target goal using a well known algorithm. * Uses mGraph which has pre-computed costs for allowed edges. It is assumed * that mGraph is already constructed. * * Should be possible to make this MT safe. * * Returns path which may be empty. path contains pathgrid points in local * cell coordinates (indoors) or world coordinates (external). * * Input params: * start, goal - pathgrid point indexes (for this cell) * * Variables: * openset - point indexes to be traversed, lowest cost at the front * closedset - point indexes already traversed * gScore - past accumulated costs vector indexed by point index * fScore - future estimated costs vector indexed by point index * * 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 size_t start, const size_t goal) const { std::deque path; if (!isPointConnected(start, goal)) { return path; // there is no path, return an empty path } 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::set closedset; openset.push_back(start); size_t current = start; while (!openset.empty()) { current = openset.front(); // front has the lowest cost openset.pop_front(); if (current == goal) break; closedset.insert(current); // remember we've been here // check all edges for the current point index for (const auto& edge : mGraph[current].edges) { if (!closedset.contains(edge.index)) { // not in closedset - i.e. have not traversed this edge destination 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]) { graphParent[dest] = current; gScore[dest] = tentative_g; 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 auto it = openset.begin(); for (; it != openset.end(); ++it) { if (fScore[*it] > fScore[dest]) break; } openset.insert(it, dest); } } } // if in closedset, i.e. traversed this edge already, try the next edge } } if (current != goal) return path; // for some reason couldn't build a path // reconstruct path to return, using local coordinates while (graphParent[current] != NoIndex) { path.push_front(mPathgrid->mPoints[current]); current = graphParent[current]; } // add first node to path explicitly path.push_front(mPathgrid->mPoints[start]); return path; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/pathgrid.hpp000066400000000000000000000037361503074453300235120ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_PATHGRID_H #define GAME_MWMECHANICS_PATHGRID_H #include #include namespace MWMechanics { class PathgridGraph { 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/pickpocket.cpp000066400000000000000000000052311503074453300240270ustar00rootroot00000000000000#include "pickpocket.hpp" #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "npcstats.hpp" namespace MWMechanics { Pickpocket::Pickpocket(const MWWorld::Ptr& thief, const MWWorld::Ptr& victim) : mThief(thief) , mVictim(victim) { } float Pickpocket::getChanceModifier(const MWWorld::Ptr& ptr, float add) { NpcStats& stats = ptr.getClass().getNpcStats(ptr); float agility = stats.getAttribute(ESM::Attribute::Agility).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float sneak = static_cast(ptr.getClass().getSkill(ptr, ESM::Skill::Sneak)); return (add + 0.2f * agility + 0.1f * luck + sneak) * stats.getFatigueTerm(); } bool Pickpocket::getDetected(float valueTerm) { float x = getChanceModifier(mThief); float y = getChanceModifier(mVictim, valueTerm); float t = 2 * x - y; float pcSneak = static_cast(mThief.getClass().getSkill(mThief, ESM::Skill::Sneak)); int iPickMinChance = MWBase::Environment::get() .getESMStore() ->get() .find("iPickMinChance") ->mValue.getInteger(); int iPickMaxChance = MWBase::Environment::get() .getESMStore() ->get() .find("iPickMaxChance") ->mValue.getInteger(); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); int roll = Misc::Rng::roll0to99(prng); if (t < pcSneak / iPickMinChance) { return (roll > int(pcSneak / iPickMinChance)); } else { t = std::min(float(iPickMaxChance), t); return (roll > int(t)); } } bool Pickpocket::pick(const MWWorld::Ptr& item, int count) { float stackValue = static_cast(item.getClass().getValue(item) * count); float fPickPocketMod = MWBase::Environment::get() .getESMStore() ->get() .find("fPickPocketMod") ->mValue.getFloat(); float valueTerm = 10 * fPickPocketMod * stackValue; return getDetected(valueTerm); } bool Pickpocket::finish() { return getDetected(0.f); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/pickpocket.hpp000066400000000000000000000012651503074453300240370ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_PICKPOCKET_H #define OPENMW_MECHANICS_PICKPOCKET_H #include "../mwworld/ptr.hpp" namespace MWMechanics { class Pickpocket { public: Pickpocket(const MWWorld::Ptr& thief, const MWWorld::Ptr& victim); /// Steal some items /// @return Was the thief detected? bool pick(const MWWorld::Ptr& item, int count); /// End the pickpocketing process /// @return Was the thief detected? bool finish(); private: bool getDetected(float valueTerm); float getChanceModifier(const MWWorld::Ptr& ptr, float add = 0); MWWorld::Ptr mThief; MWWorld::Ptr mVictim; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/recharge.cpp000066400000000000000000000105331503074453300234540ustar00rootroot00000000000000#include "recharge.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.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() .getESMStore() ->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.getCellRef().getCount()) return false; MWWorld::Ptr player = MWMechanics::getPlayer(); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); MWBase::Environment::get().getWorld()->breakInvisibility(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; 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); float restored = creature->mData.mSoul * (roll / x); 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; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/recharge.hpp000066400000000000000000000004531503074453300234610ustar00rootroot00000000000000#ifndef MWMECHANICS_RECHARGE_H #define MWMECHANICS_RECHARGE_H #include "../mwworld/ptr.hpp" namespace MWMechanics { bool rechargeItem(const MWWorld::Ptr& item, const float maxCharge, const float duration); bool rechargeItem(const MWWorld::Ptr& item, const MWWorld::Ptr& gem); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/repair.cpp000066400000000000000000000107041503074453300231560ustar00rootroot00000000000000#include "repair.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.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(); MWBase::Environment::get().getWorld()->breakInvisibility(player); // unstack tool if required player.getClass().getContainerStore(player).unstack(mTool); // reduce number of uses left int uses = mTool.getClass().getItemHealth(mTool); uses -= std::min(uses, 1); mTool.getCellRef().setCharge(uses); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); 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 fRepairAmountMult = MWBase::Environment::get() .getESMStore() ->get() .find("fRepairAmountMult") ->mValue.getFloat(); float toolQuality = ref->mBase->mData.mQuality; float x = (0.1f * pcStrength + 0.1f * pcLuck + armorerSkill) * fatigueTerm; 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); // repair by 'y' points int charge = itemToRepair.getClass().getItemHealth(itemToRepair); charge = std::min(charge + y, itemToRepair.getClass().getItemMaxHealth(itemToRepair)); itemToRepair.getCellRef().setCharge(charge); // attempt to re-stack item, in case it was fully repaired MWWorld::ContainerStoreIterator stacked = player.getClass().getContainerStore(player).restack(itemToRepair); // 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); // increase skill player.getClass().skillUsageSucceeded(player, ESM::Skill::Armorer, ESM::Skill::Armorer_Repair); 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}"); } // tool used up? if (mTool.getCellRef().getCharge() == 0) { MWWorld::ContainerStore& store = player.getClass().getContainerStore(player); store.remove(mTool, 1); std::string message = MWBase::Environment::get() .getESMStore() ->get() .find("sNotifyMessage51") ->mValue.getString(); message = Misc::StringUtils::format(message, mTool.getClass().getName(mTool)); 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) { if (iter->getCellRef().getRefId() == mTool.getCellRef().getRefId()) { mTool = *iter; MWBase::Environment::get().getWindowManager()->playSound(ESM::RefId::stringRefId("Item Repair Up")); break; } } } } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/repair.hpp000066400000000000000000000006121503074453300231600ustar00rootroot00000000000000#ifndef OPENMW_MWMECHANICS_REPAIR_H #define OPENMW_MWMECHANICS_REPAIR_H #include "../mwworld/ptr.hpp" namespace MWMechanics { class Repair { public: void setTool(const MWWorld::Ptr& tool) { mTool = tool; } MWWorld::Ptr getTool() { return mTool; } void repair(const MWWorld::Ptr& itemToRepair); private: MWWorld::Ptr mTool; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/security.cpp000066400000000000000000000111731503074453300235440ustar00rootroot00000000000000#include "security.hpp" #include #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "creaturestats.hpp" namespace MWMechanics { Security::Security(const MWWorld::Ptr& actor) : mActor(actor) { CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); mAgility = creatureStats.getAttribute(ESM::Attribute::Agility).getModified(); mLuck = creatureStats.getAttribute(ESM::Attribute::Luck).getModified(); mSecuritySkill = static_cast(actor.getClass().getSkill(actor, ESM::Skill::Security)); mFatigueTerm = creatureStats.getFatigueTerm(); } void Security::pickLock(const MWWorld::Ptr& lock, const MWWorld::Ptr& lockpick, std::string_view& resultMessage, std::string_view& resultSound) { // 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); if (uses == 0) return; int lockStrength = lock.getCellRef().getLockLevel(); float pickQuality = lockpick.get()->mBase->mData.mQuality; float fPickLockMult = MWBase::Environment::get() .getESMStore() ->get() .find("fPickLockMult") ->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x *= pickQuality * mFatigueTerm; x += fPickLockMult * lockStrength; MWBase::Environment::get().getMechanicsManager()->unlockAttempted(mActor, lock); resultSound = "Open Lock Fail"; if (x <= 0) resultMessage = "#{sLockImpossible}"; else { 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, ESM::Skill::Security_PickLock); } else resultMessage = "#{sLockFail}"; } lockpick.getCellRef().setCharge(--uses); if (!uses) lockpick.getContainerStore()->remove(lockpick, 1); } 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; int uses = probe.getClass().getItemHealth(probe); if (uses == 0) return; float probeQuality = probe.get()->mBase->mData.mQuality; const ESM::Spell* trapSpell = MWBase::Environment::get().getESMStore()->get().find(trap.getCellRef().getTrap()); int trapSpellPoints = trapSpell->mData.mCost; float fTrapCostMult = MWBase::Environment::get() .getESMStore() ->get() .find("fTrapCostMult") ->mValue.getFloat(); float x = 0.2f * mAgility + 0.1f * mLuck + mSecuritySkill; x += fTrapCostMult * trapSpellPoints; x *= probeQuality * mFatigueTerm; MWBase::Environment::get().getMechanicsManager()->unlockAttempted(mActor, trap); resultSound = "Disarm Trap Fail"; if (x <= 0) resultMessage = "#{sTrapImpossible}"; else { auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (Misc::Rng::roll0to99(prng) <= x) { trap.getCellRef().setTrap(ESM::RefId()); resultSound = "Disarm Trap"; resultMessage = "#{sTrapSuccess}"; mActor.getClass().skillUsageSucceeded(mActor, ESM::Skill::Security, ESM::Skill::Security_DisarmTrap); } else resultMessage = "#{sTrapFail}"; } probe.getCellRef().setCharge(--uses); if (!uses) probe.getContainerStore()->remove(probe, 1); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/security.hpp000066400000000000000000000012501503074453300235440ustar00rootroot00000000000000#ifndef MWMECHANICS_SECURITY_H #define MWMECHANICS_SECURITY_H #include "../mwworld/ptr.hpp" namespace MWMechanics { /// @brief implementation of Security skill class Security { public: Security(const MWWorld::Ptr& actor); 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; MWWorld::Ptr mActor; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/setbaseaisetting.hpp000066400000000000000000000020721503074453300252360ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellcasting.cpp000066400000000000000000000614021503074453300243650ustar00rootroot00000000000000#include "spellcasting.hpp" #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 "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwrender/animation.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "spelleffects.hpp" #include "spellutil.hpp" #include "weapontype.hpp" namespace MWMechanics { CastSpell::CastSpell( const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool scriptedSpell) : mCaster(caster) , mTarget(target) , mFromProjectile(fromProjectile) , 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(VFS::Path::Normalized(areaStatic->mModel)), texture, mHitPosition, 1.0f); continue; } else world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(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() const { osg::Vec3f fallbackDirection(0, 1, 0); osg::Vec3f offset(0, 0, 0); if (!mTarget.isEmpty() && mTarget.getClass().isActor()) offset.z() = MWBase::Environment::get().getWorld()->getHalfExtents(mTarget).z(); // 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()); MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection, mItem); } 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) { const auto& stats = target.getClass().getCreatureStats(target); if (stats.isDead() && stats.isDeathAnimationFinished()) targetIsDeadActor = true; } // If none of the effects need to apply, we can early-out bool found = false; bool containsRecastable = false; const auto& store = MWBase::Environment::get().getESMStore()->get(); for (const ESM::IndexedENAMstruct& effect : effects.mList) { if (effect.mData.mRange == range) { found = true; const ESM::MagicEffect* magicEffect = store.find(effect.mData.mEffectID); if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable)) containsRecastable = true; } } if (!found) return; ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem); params.setFlag(mFlags); bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer()); const ActiveSpells* targetSpells = nullptr; if (targetIsActor) targetSpells = &target.getClass().getCreatureStats(target).getActiveSpells(); // Re-casting a bound equipment effect has no effect if the spell is still active if (!containsRecastable && targetSpells && targetSpells->isSpellActive(mId)) { if (castByPlayer) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicCannotRecast}"); return; } for (auto& enam : effects.mList) { if (target.isEmpty()) break; if (enam.mData.mRange != range) continue; 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 && (mCaster.isEmpty() || !mCaster.getClass().isActor())) continue; 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) { // If player is attempting to cast a harmful spell on or is healing a living target, show the target's // HP bar. MWBase::Environment::get().getWindowManager()->setEnemy(target); } if (!targetIsActor && magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) { playEffects(target, *magicEffect); } } if (!exploded) explodeSpell(effects, target, range); if (!target.isEmpty()) { if (!params.getEffects().empty()) { if (targetIsActor) { if (!targetIsDeadActor) target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params); } else { // Apply effects instantly. We can ignore effect deletion since the entire params object gets // deleted afterwards anyway and we can ignore reflection since non-actors cannot reflect spells for (auto& effect : params.getEffects()) applyMagicEffect(target, mCaster, params, effect, 0.f); } } } } bool CastSpell::cast(const ESM::RefId& id) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (const auto spell = store.get().search(id)) return cast(spell); if (const auto potion = store.get().search(id)) return cast(potion); if (const auto ingredient = store.get().search(id)) return cast(ingredient); throw std::runtime_error("ID type cannot be casted"); } bool CastSpell::cast(const MWWorld::Ptr& item, bool launchProjectile) { 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 auto& store = MWBase::Environment::get().getESMStore(); const ESM::Enchantment* enchantment = store->get().find(enchantmentName); // 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.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(type)->mWeaponClass; isProjectile = (weapclass == ESM::WeaponType::Thrown || weapclass == ESM::WeaponType::Ammo); } int type = enchantment->mData.mType; // Check if there's enough charge left if (!godmode && (type == ESM::Enchantment::WhenUsed || (!isProjectile && type == ESM::Enchantment::WhenStrikes))) { int castCost = getEffectiveEnchantmentCastCost(*enchantment, mCaster); if (item.getCellRef().getEnchantmentCharge() == -1) item.getCellRef().setEnchantmentCharge( static_cast(MWMechanics::getEnchantmentCharge(*enchantment))); if (item.getCellRef().getEnchantmentCharge() < castCost) { if (mCaster == getPlayer()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicInsufficientCharge}"); // Failure sound ESM::RefId school = ESM::Skill::Alteration; if (!enchantment->mEffects.mList.empty()) { short effectId = enchantment->mEffects.mList.front().mData.mEffectID; const ESM::MagicEffect* magicEffect = store->get().find(effectId); school = magicEffect->mData.mSchool; } MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); sndMgr->playSound3D( mCaster, store->get().find(school)->mSchool->mFailureSound, 1.0f, 1.0f); } return false; } // Reduce charge item.getCellRef().setEnchantmentCharge(item.getCellRef().getEnchantmentCharge() - castCost); } if (type == ESM::Enchantment::WhenUsed) { if (mCaster == getPlayer()) mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_UseMagicItem); } else if (type == ESM::Enchantment::CastOnce) { if (!godmode) item.getContainerStore()->remove(item, 1); } else if (type == ESM::Enchantment::WhenStrikes) { if (mCaster == getPlayer()) mCaster.getClass().skillUsageSucceeded(mCaster, ESM::Skill::Enchant, ESM::Skill::Enchant_CastOnStrike); } if (isProjectile) inflict(mTarget, enchantment->mEffects, ESM::RT_Self); else inflict(mCaster, enchantment->mEffects, ESM::RT_Self); if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, enchantment->mEffects, ESM::RT_Touch); if (launchProjectile) launchMagicBolt(); else if (isProjectile || !mTarget.isEmpty()) inflict(mTarget, enchantment->mEffects, ESM::RT_Target); return true; } bool CastSpell::cast(const ESM::Potion* potion) { mSourceName = potion->mName; mId = potion->mId; mFlags = static_cast( ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); // 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; } bool CastSpell::cast(const ESM::Spell* spell) { mSourceName = spell->mName; mId = spell->mId; ESM::RefId school = ESM::Skill::Alteration; bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mScriptedSpell) { school = getSpellSchool(spell, mCaster); CreatureStats& stats = mCaster.getClass().getCreatureStats(mCaster); if (!godmode) { bool fail = false; // Check success float successChance = getSpellSuccessChance(spell, mCaster, nullptr, true, false); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); if (Misc::Rng::roll0to99(prng) >= successChance) { if (mCaster == getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicSkillFail}"); fail = true; } if (fail) { // Failure sound 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; } } // A power can be used once per 24h if (spell->mData.mType == ESM::Spell::ST_Power) stats.getSpells().usePower(spell); } 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, spell->mEffects, ESM::RT_Self); if (!mTarget.isEmpty()) inflict(mTarget, spell->mEffects, ESM::RT_Touch); launchMagicBolt(); return true; } bool CastSpell::cast(const ESM::Ingredient* ingredient) { mId = ingredient->mId; mFlags = static_cast( ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable); mSourceName = ingredient->mName; std::optional effect = rollIngredientEffect(mCaster, ingredient, 0); if (effect) { inflict(mCaster, *effect, ESM::RT_Self); return true; } if (mCaster == getPlayer()) { // "X has no effect on you" 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; } void CastSpell::playSpellCastingEffects(const ESM::Enchantment* enchantment) const { playSpellCastingEffects(enchantment->mEffects.mList); } void CastSpell::playSpellCastingEffects(const ESM::Spell* spell) const { playSpellCastingEffects(spell->mEffects.mList); } void CastSpell::playSpellCastingEffects(const std::vector& effects) const { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); std::vector addedEffects; for (const ESM::IndexedENAMstruct& effectData : effects) { const auto effect = store.get().find(effectData.mData.mEffectID); const ESM::Static* castStatic; if (!effect->mCasting.empty()) castStatic = store.get().find(effect->mCasting); else castStatic = store.get().find(ESM::RefId::stringRefId("VFX_DefaultCast")); VFS::Path::Normalized castStaticModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); // check if the effect was already added if (std::find(addedEffects.begin(), addedEffects.end(), castStaticModel) != addedEffects.end()) continue; MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { animation->addEffect(castStaticModel.value(), ESM::MagicEffect::indexToName(effect->mIndex), false, {}, effect->mParticle); } else { // If the caster has no animation, add the effect directly to the effectManager // 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(castStaticModel, effect->mParticle, pos, scale); } if (animation && !mCaster.getClass().isActor()) animation->addSpellCastGlow(effect->getColor()); addedEffects.push_back(std::move(castStaticModel)); MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); if (!effect->mCastSound.empty()) sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f); else 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) { const VFS::Path::Normalized castStaticModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(castStatic->mModel)); anim->addEffect(castStaticModel.value(), ESM::MagicEffect::indexToName(magicEffect.mIndex), loop, {}, magicEffect.mParticle); } } } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellcasting.hpp000066400000000000000000000054051503074453300243730ustar00rootroot00000000000000#ifndef MWMECHANICS_SPELLCASTING_H #define MWMECHANICS_SPELLCASTING_H #include #include #include "../mwworld/ptr.hpp" namespace ESM { struct Spell; struct Ingredient; struct Potion; struct EffectList; struct Enchantment; struct MagicEffect; } namespace MWMechanics { struct EffectKey; class CastSpell { private: MWWorld::Ptr mCaster; // May be empty MWWorld::Ptr mTarget; // May be empty 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: 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) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) 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 }; CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile = false, const bool scriptedSpell = false); 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); /// @note mCaster must be an NPC bool cast(const ESM::Ingredient* ingredient); bool cast(const ESM::Potion* potion); /// @note Auto detects if spell, ingredient or potion bool cast(const ESM::RefId& id); void playSpellCastingEffects(const ESM::Enchantment* enchantment) const; void playSpellCastingEffects(const ESM::Spell* spell) const; /// @note \a target can be any type of object, not just actors. 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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/spelleffects.cpp000066400000000000000000001711241503074453300243570ustar00rootroot00000000000000#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()) { const VFS::Path::Normalized absorbStaticModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(absorbStatic->mModel)); animation->addEffect( absorbStaticModel.value(), 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 != nullptr) { const VFS::Path::Normalized fxModel = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)); anim->addEffect(fxModel.value(), ""); } } } 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)); } } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spelleffects.hpp000066400000000000000000000021471503074453300243620ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwmechanics/spelllist.cpp000066400000000000000000000116021503074453300237050ustar00rootroot00000000000000#include "spelllist.hpp" #include #include #include #include #include "spells.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" namespace { template const std::vector getSpellList(const ESM::RefId& id) { return MWBase::Environment::get().getESMStore()->get().find(id)->mSpells.mList; } template bool withBaseRecord(const ESM::RefId& id, const std::function&)>& function) { T copy = *MWBase::Environment::get().getESMStore()->get().find(id); bool changed = function(copy.mSpells.mList); if (changed) MWBase::Environment::get().getESMStore()->overrideRecord(copy); return changed; } } namespace MWMechanics { SpellList::SpellList(const ESM::RefId& id, int type) : mId(id) , mType(type) { } bool SpellList::withBaseRecord(const std::function&)>& function) { 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.toDebugString()); } } const std::vector SpellList::getSpells() const { 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.toDebugString()); } } const ESM::Spell* SpellList::getSpell(const ESM::RefId& id) { return MWBase::Environment::get().getESMStore()->get().find(id); } void SpellList::add(const ESM::Spell* spell) { auto& id = spell->mId; bool changed = withBaseRecord([&](auto& spells) { for (const auto& it : spells) { if (id == it) return false; } spells.push_back(id); return true; }); if (changed) { for (auto listener : mListeners) listener->addSpell(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++) { if (id == *it) { spells.erase(it); return true; } } return false; }); if (changed) { for (auto listener : mListeners) listener->removeSpell(spell); } } 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 spell == id; }; return ids.end() != std::find_if(ids.begin(), ids.end(), isSpell); }); if (it == spells.end()) return false; spells.erase(it, spells.end()); return true; }); if (changed) { for (auto listener : mListeners) { for (auto& id : ids) { const auto spell = getSpell(id); listener->removeSpell(spell); } } } } void SpellList::clear() { bool changed = withBaseRecord([](auto& spells) { if (spells.empty()) return false; spells.clear(); return true; }); if (changed) { for (auto listener : mListeners) listener->removeAllSpells(); } } void SpellList::addListener(Spells* spells) { if (std::find(mListeners.begin(), mListeners.end(), spells) != mListeners.end()) return; mListeners.push_back(spells); } void SpellList::removeListener(Spells* spells) { const auto it = std::find(mListeners.begin(), mListeners.end(), spells); if (it != mListeners.end()) mListeners.erase(it); } void SpellList::updateListener(Spells* before, Spells* after) { const auto it = std::find(mListeners.begin(), mListeners.end(), before); if (it == mListeners.end()) return mListeners.push_back(after); *it = after; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spelllist.hpp000066400000000000000000000040511503074453300237120ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_SPELLLIST_H #define GAME_MWMECHANICS_SPELLLIST_H #include #include #include #include #include #include namespace ESM { struct SpellState; } namespace MWMechanics { 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, /// 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. /// @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 { ESM::RefId mId; const int mType; std::vector mListeners; bool withBaseRecord(const std::function&)>& function); public: SpellList(const ESM::RefId& id, int type); /// 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 remove(const ESM::Spell* spell); void removeAll(const std::vector& spells); void clear(); ///< Remove all spells of all types. void addListener(Spells* spells); void removeListener(Spells* spells); void updateListener(Spells* before, Spells* after); const std::vector getSpells() const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellpriority.cpp000066400000000000000000000767611503074453300246340ustar00rootroot00000000000000#include "spellpriority.hpp" #include "weaponpriority.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "creaturestats.hpp" #include "spellresistance.hpp" #include "spellutil.hpp" #include "summoning.hpp" #include "weapontype.hpp" namespace { int numEffectsToDispel(const MWWorld::Ptr& actor, int effectFilter = -1, bool negative = true) { 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 (effectFilter == -1) { 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; for (const auto& effect : params.getEffects()) { int effectId = effect.mEffectId; if (effectFilter != -1 && effectId != effectFilter) continue; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(effectId); if (effect.mDuration <= 3) // Don't attempt to dispel if effect runs out shortly anyway continue; if (negative && magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) ++toCure; if (!negative && !(magicEffect->mData.mFlags & ESM::MagicEffect::Harmful)) ++toCure; } } return toCure; } 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->getSourceSpellId() != spellId) continue; const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it; for (const auto& effect : params.getEffects()) { 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 types = 0; for (const ESM::IndexedENAMstruct& effect : effects.mList) { if (effect.mData.mRange == ESM::RT_Self) types |= RangeTypes::Self; else if (effect.mData.mRange == ESM::RT_Touch) types |= RangeTypes::Touch; else if (effect.mData.mRange == ESM::RT_Target) types |= RangeTypes::Target; } return types; } float ratePotion(const MWWorld::Ptr& item, const MWWorld::Ptr& actor) { 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, bool checkMagicka) { float successChance = MWMechanics::getSpellSuccessChance(spell, actor, nullptr, true, checkMagicka); if (successChance == 0.f) return 0.f; if (spell->mData.mType != ESM::Spell::ST_Spell) return 0.f; // Don't make use of racial bonus spells, like MW. Can be made optional later if (actor.getClass().isNpc()) { 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) && isSpellActive(actor, actor, spell->mId)) return 0.f; 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) { if (ptr.getClass().getEnchantment(ptr).empty()) return 0.f; 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())) return 0.f; if (types & (Touch | Target) && getSpellDuration(enemy, ptr.getCellRef().getRefId()) > 3) return 0.f; if (enchantment->mData.mType == ESM::Enchantment::CastOnce) { return rateEffects(enchantment->mEffects, actor, enemy); } else if (enchantment->mData.mType == ESM::Enchantment::WhenUsed) { MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); // 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(*enchantment, actor); if (ptr.getCellRef().getEnchantmentCharge() != -1 && ptr.getCellRef().getEnchantmentCharge() < castCost) return 0.f; float rating = rateEffects(enchantment->mEffects, actor, enemy); 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) { // 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::Blind: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't attack if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't attack if (stats.getDrawState() != MWMechanics::DrawState::Weapon) return 0.f; break; } case ESM::MagicEffect::Sound: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't cast spells 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) return 0.f; break; } case ESM::MagicEffect::Silence: { if (enemy.isEmpty()) return 0.f; const CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); // Enemy can't cast spells if (stats.isParalyzed() || stats.getKnockedDown()) return 0.f; // Enemy doesn't cast spells 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::Burden: { if (enemy.isEmpty()) return 0.f; // Ignore enemy without inventory if (!enemy.getClass().hasInventoryStore(enemy)) return 0.f; // burden makes sense only to overburden an enemy float burden = enemy.getClass().getEncumbrance(enemy) - enemy.getClass().getCapacity(enemy); if (burden > 0) return 0.f; if ((effect.mMagnMin + effect.mMagnMax) / 2.f > -burden) rating *= 3; else return 0.f; break; } case ESM::MagicEffect::Feather: { // Ignore actors without inventory if (!actor.getClass().hasInventoryStore(actor)) return 0.f; // feather makes sense only for overburden actors float burden = actor.getClass().getEncumbrance(actor) - actor.getClass().getCapacity(actor); if (burden <= 0) return 0.f; if ((effect.mMagnMin + effect.mMagnMax) / 2.f >= burden) rating *= 3; else return 0.f; 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 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; 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::AbsorbMagicka: if (!enemy.isEmpty() && enemy.getClass().getCreatureStats(enemy).getMagicka().getCurrent() <= 0.f) { rating = 0.5f; rating *= getRestoreMagickaPriority(actor); } 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; } 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; case ESM::MagicEffect::Dispel: { int numPositive = 0; int numNegative = 0; int diff = 0; 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); diff = numPositive - numNegative; // if rating < 0 here, the spell will be considered as negative later rating *= -1; } if (diff <= 0) return 0.f; rating *= (diff) / 5.f; break; } // 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: { if (enemy.isEmpty()) return 0.f; // Ignore enemy without inventory if (!enemy.getClass().hasInventoryStore(enemy)) return 0.f; MWWorld::InventoryStore& inv = enemy.getClass().getInventoryStore(enemy); // According to UESP static const int armorSlots[] = { 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, }; bool enemyHasArmor = false; // Ignore enemy without armor for (unsigned int i = 0; i < sizeof(armorSlots) / sizeof(int); ++i) { MWWorld::ContainerStoreIterator item = inv.getSlot(armorSlots[i]); if (item != inv.end() && (item.getType() == MWWorld::ContainerStore::Type_Armor)) { enemyHasArmor = true; break; } } if (!enemyHasArmor) return 0.f; break; } case ESM::MagicEffect::DisintegrateWeapon: { if (enemy.isEmpty()) return 0.f; // Ignore enemy without inventory if (!enemy.getClass().hasInventoryStore(enemy)) return 0.f; MWWorld::InventoryStore& inv = enemy.getClass().getInventoryStore(enemy); MWWorld::ContainerStoreIterator item = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); // Ignore enemy without weapons if (item == inv.end() || (item.getType() != MWWorld::ContainerStore::Type_Weapon)) return 0.f; break; } 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; { 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; 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; } // Allow only one summoned creature at time if (isSummoningEffect(effect.mEffectID)) { MWMechanics::CreatureStats& creatureStats = actor.getClass().getCreatureStats(actor); 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 if (effect.mRange == ESM::RT_Target) { if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.75f)) return 0.f; if (enemy.isEmpty()) return 0.f; if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(enemy), 0.75f)) return 0.f; } const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(effect.mEffectID); if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { rating *= -1.f; if (enemy.isEmpty()) return 0.f; // Check resistance for harmful effects CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); float resistance = MWMechanics::getEffectResistanceAttribute(effect.mEffectID, &stats.getMagicEffects()); rating *= (1.f - std::min(resistance, 100.f) / 100.f); } // for harmful no-magnitude effects (e.g. silence) check if enemy is already has them // for non-harmful no-magnitude effects (e.g. bound items) check if actor is already has them if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) { if (magicEffect->mData.mFlags & ESM::MagicEffect::Harmful) { CreatureStats& stats = enemy.getClass().getCreatureStats(enemy); if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0) return 0.f; } else { CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getMagicEffects().getOrDefault(effect.mEffectID).getMagnitude() > 0) return 0.f; } } rating *= calcEffectCost(effect, magicEffect); // Currently treating all "on target" or "on touch" effects to target the enemy actor. // Combat AI is egoistic, so doesn't consider applying positive effects to friendly actors. if (effect.mRange != ESM::RT_Self) rating *= -1.f; return rating; } 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; 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 (const ESM::IndexedENAMstruct& effect : list.mList) { 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().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) { if (effectIt->mData.mRange == ESM::RT_Target) { if (!MWBase::Environment::get().getWorld()->isSwimming(enemy)) mult = fAIRangeMagicSpellMult; else mult = 0.0f; break; } } return MWMechanics::getSpellSuccessChance(spell, actor) * mult; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellpriority.hpp000066400000000000000000000023221503074453300246170ustar00rootroot00000000000000#ifndef OPENMW_SPELL_PRIORITY_H #define OPENMW_SPELL_PRIORITY_H namespace ESM { struct Spell; struct EffectList; struct ENAMstruct; } namespace MWWorld { class Ptr; } namespace MWMechanics { // RangeTypes using bitflags to allow multiple range types, as can be the case with spells having multiple effects. enum RangeTypes { Self = 0x1, Touch = 0x10, Target = 0x100 }; int getRangeTypes(const ESM::EffectList& effects); 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); /// @note target may be empty 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); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellresistance.cpp000066400000000000000000000071221503074453300250740ustar00rootroot00000000000000#include "spellresistance.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "creaturestats.hpp" #include "spellutil.hpp" namespace MWMechanics { float getEffectMultiplier(short effectId, const MWWorld::Ptr& actor, const MWWorld::Ptr& caster, const ESM::Spell* spell, const MagicEffects* effects) { if (!actor.getClass().isActor()) return 1; float resistance = getEffectResistance(effectId, actor, caster, spell, effects); return 1 - resistance / 100.f; } 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().getESMStore()->get().find(effectId); const MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); const MWMechanics::MagicEffects* magicEffects = &stats.getMagicEffects(); if (effects) magicEffects = effects; float resistance = getEffectResistanceAttribute(effectId, magicEffects); float willpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float luck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float x = (willpower + 0.1f * luck) * stats.getFatigueTerm(); // This makes spells that are easy to cast harder to resist and vice versa float castChance = 100.f; if (spell != nullptr && !caster.isEmpty() && caster.getClass().isActor()) castChance = getSpellSuccessChance(spell, caster, nullptr, false, false); // Uncapped casting chance if (castChance > 0) x *= 50 / castChance; auto& prng = MWBase::Environment::get().getWorld()->getPrng(); float roll = Misc::Rng::rollClosedProbability(prng) * 100; if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) roll -= resistance; if (x <= roll) x = 0; else { if (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude) x = 100; else x = roll / std::min(x, 100.f); } x = std::min(x + resistance, 100.f); return x; } 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->getOrDefault(resistanceEffect).getMagnitude(); if (weaknessEffect != -1) resistance -= actorEffects->getOrDefault(weaknessEffect).getMagnitude(); if (effectId == ESM::MagicEffect::FireDamage) resistance += actorEffects->getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (effectId == ESM::MagicEffect::ShockDamage) resistance += actorEffects->getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (effectId == ESM::MagicEffect::FrostDamage) resistance += actorEffects->getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); return resistance; } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellresistance.hpp000066400000000000000000000031601503074453300250770ustar00rootroot00000000000000#ifndef MWMECHANICS_SPELLRESISTANCE_H #define MWMECHANICS_SPELLRESISTANCE_H namespace ESM { struct Spell; } namespace MWWorld { class Ptr; } namespace MWMechanics { class MagicEffects; /// Get an effect multiplier for applying an effect cast by the given actor in the given spell (optional). /// @return effect multiplier from 0 to 2. (100% net resistance to 100% net weakness) /// @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); /// 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); /// 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); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/spells.cpp000066400000000000000000000242211503074453300231750ustar00rootroot00000000000000#include "spells.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" #include "stat.hpp" namespace MWMechanics { Spells::Spells() {} Spells::Spells(const Spells& spells) : mSpellList(spells.mSpellList) , mSpells(spells.mSpells) , mSelectedSpell(spells.mSelectedSpell) , mUsedPowers(spells.mUsedPowers) { if (mSpellList) mSpellList->addListener(this); } Spells::Spells(Spells&& spells) : mSpellList(std::move(spells.mSpellList)) , mSpells(std::move(spells.mSpells)) , mSelectedSpell(std::move(spells.mSelectedSpell)) , mUsedPowers(std::move(spells.mUsedPowers)) { if (mSpellList) mSpellList->updateListener(&spells, this); } std::vector::const_iterator Spells::begin() const { return mSpells.begin(); } std::vector::const_iterator Spells::end() const { return mSpells.end(); } bool Spells::hasSpell(const ESM::RefId& spell) const { return hasSpell(SpellList::getSpell(spell)); } bool Spells::hasSpell(const ESM::Spell* spell) const { return std::find(mSpells.begin(), mSpells.end(), spell) != mSpells.end(); } void Spells::add(const ESM::Spell* spell, bool modifyBase) { if (modifyBase) mSpellList->add(spell); else addSpell(spell); } void Spells::add(const ESM::RefId& spellId, bool modifyBase) { add(SpellList::getSpell(spellId), modifyBase); } void Spells::addSpell(const ESM::Spell* spell) { if (!hasSpell(spell)) mSpells.emplace_back(spell); } void Spells::remove(const ESM::RefId& spellId, bool modifyBase) { remove(SpellList::getSpell(spellId), modifyBase); } void Spells::remove(const ESM::Spell* spell, bool modifyBase) { removeSpell(spell); if (modifyBase) mSpellList->remove(spell); if (spell->mId == mSelectedSpell) mSelectedSpell = ESM::RefId(); } void Spells::removeSpell(const ESM::Spell* spell) { const auto it = std::find(mSpells.begin(), mSpells.end(), spell); if (it != mSpells.end()) mSpells.erase(it); } void Spells::removeAllSpells() { mSpells.clear(); } void Spells::clear(bool modifyBase) { removeAllSpells(); if (modifyBase) mSpellList->clear(); } void Spells::setSelectedSpell(const ESM::RefId& spellId) { mSelectedSpell = spellId; } const ESM::RefId& Spells::getSelectedSpell() const { return mSelectedSpell; } bool Spells::hasSpellType(const ESM::Spell::SpellType type) const { 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 hasSpellType(ESM::Spell::ST_Disease); } bool Spells::hasBlightDisease() const { return hasSpellType(ESM::Spell::ST_Blight); } void Spells::purge(const SpellFilter& filter) { std::vector purged; for (auto iter = mSpells.begin(); iter != mSpells.end();) { const ESM::Spell* spell = *iter; if (filter(spell)) { iter = mSpells.erase(iter); purged.push_back(spell->mId); } else ++iter; } if (!purged.empty()) mSpellList->removeAll(purged); } void Spells::purgeCommonDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Disease; }); } void Spells::purgeBlightDisease() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Blight && !hasCorprusEffect(spell); }); } void Spells::purgeCorprusDisease() { purge(&hasCorprusEffect); } void Spells::purgeCurses() { purge([](auto spell) { return spell->mData.mType == ESM::Spell::ST_Curse; }); } bool Spells::hasCorprusEffect(const ESM::Spell* spell) { for (const auto& effectIt : spell->mEffects.mList) { if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus) { return true; } } return false; } bool Spells::canUsePower(const ESM::Spell* spell) const { 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) { // 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) { const auto& baseSpells = mSpellList->getSpells(); 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().getESMStore()->get().search(id); if (spell) { addSpell(spell); if (id == state.mSelectedSpell) mSelectedSpell = id; } } // Add spells from the base record for (const ESM::RefId& id : baseSpells) { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(id); if (spell) addSpell(spell); } for (auto it = state.mUsedPowers.begin(); it != state.mUsedPowers.end(); ++it) { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(it->first); if (!spell) continue; mUsedPowers.emplace_back(spell, MWWorld::TimeStamp(it->second)); } // 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().getESMStore()->get().search(it->first); if (!spell) continue; // Import data only for player, other actors should not suffer from corprus worsening. MWWorld::Ptr player = getPlayer(); if (creatureStats->getActorId() != player.getClass().getCreatureStats(player).getActorId()) return; // 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 (info.mId == ESM::MagicEffect::FortifyAttribute) { 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 (info.mId == ESM::MagicEffect::DrainAttribute) { 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 { const auto& baseSpells = mSpellList->getSpells(); for (const auto spell : mSpells) { // Don't save spells and powers stored in the base record 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()) { state.mSpells.emplace_back(spell->mId); } } state.mSelectedSpell = mSelectedSpell; for (const auto& it : mUsedPowers) state.mUsedPowers[it.first->mId] = it.second.toEsm(); } bool Spells::setSpells(const ESM::RefId& actorId) { bool result; 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) { for (const ESM::RefId& id : spells) { const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(id); if (spell) addSpell(spell); else Log(Debug::Warning) << "Warning: ignoring nonexistent spell " << id; } } Spells::~Spells() { if (mSpellList) mSpellList->removeListener(this); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spells.hpp000066400000000000000000000064021503074453300232030ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_SPELLS_H #define GAME_MWMECHANICS_SPELLS_H #include #include #include #include #include "../mwworld/timestamp.hpp" #include "spelllist.hpp" namespace ESM { struct SpellState; } namespace MWMechanics { class CreatureStats; class MagicEffects; /// \brief Spell list /// /// This class manages known spells as well as abilities, powers and permanent negative effects like /// diseases. It also keeps track of used powers (which can only be used every 24h). class Spells { std::shared_ptr mSpellList; std::vector mSpells; // Note: this is the spell that's about to be cast, *not* the spell selected in the GUI (which may be different) ESM::RefId mSelectedSpell; std::vector> mUsedPowers; bool hasSpellType(const ESM::Spell::SpellType type) const; using SpellFilter = bool (*)(const ESM::Spell*); void purge(const SpellFilter& filter); void addSpell(const ESM::Spell* spell); void removeSpell(const ESM::Spell* spell); void removeAllSpells(); friend class SpellList; public: using Collection = std::vector; Spells(); Spells(const Spells&); Spells(Spells&& spells); ~Spells(); static bool hasCorprusEffect(const ESM::Spell* spell); bool canUsePower(const ESM::Spell* spell) const; void usePower(const ESM::Spell* spell); void purgeCommonDisease(); void purgeBlightDisease(); void purgeCorprusDisease(); void purgeCurses(); Collection::const_iterator begin() const; Collection::const_iterator end() const; bool hasSpell(const ESM::RefId& spell) const; bool hasSpell(const ESM::Spell* spell) const; void add(const ESM::RefId& spell, bool modifyBase = true); ///< Adding a spell that is already listed in *this is a no-op. void add(const ESM::Spell* spell, bool modifyBase = true); ///< Adding a spell that is already listed in *this is a no-op. void remove(const ESM::RefId& spell, bool modifyBase = true); void remove(const ESM::Spell* spell, bool modifyBase = true); ///< If the spell to be removed is the selected spell, the selected spell will be changed to /// no spell (empty id). void clear(bool modifyBase = false); ///< Remove all spells of all types. void setSelectedSpell(const ESM::RefId& spellId); ///< This function does not verify, if the spell is available. const ESM::RefId& getSelectedSpell() const; ///< May return an empty id. bool hasCommonDisease() const; bool hasBlightDisease() 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; bool setSpells(const ESM::RefId& id); void addAllToInstance(const std::vector& spells); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellutil.cpp000066400000000000000000000315351503074453300237160ustar00rootroot00000000000000#include "spellutil.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "actorutil.hpp" #include "creaturestats.hpp" namespace MWMechanics { namespace { float getTotalCost(const ESM::EffectList& list, const EffectCostMethod method = EffectCostMethod::GameSpell) { 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, const EffectCostMethod method) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); if (!magicEffect) magicEffect = store.get().find(effect.mEffectID); bool hasMagnitude = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude); bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration); 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(); 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 *= 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; float cost = getTotalCost(spell.mEffects); return std::round(cost); } int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr& actor) { /* * Each point of enchant skill above/under 10 subtracts/adds * one percent of enchantment cost while minimum is 1. */ int eSkill = actor.getClass().getSkill(actor, ESM::Skill::Enchant); const float result = castCost - (castCost / 100) * (eSkill - 10); return static_cast((result < 1) ? 1 : result); } 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::IndexedENAMstruct& effect : spell->mEffects.mList) { 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.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() .getESMStore() ->get() .find("fEffectCostMult") ->mValue.getFloat(); x *= fEffectCostMult; float s = 2.0f * actor.getClass().getSkill(actor, magicEffect->mData.mSchool); if (s - x < y) { y = s - x; if (effectiveSchool) *effectiveSchool = magicEffect->mData.mSchool; lowestSkill = s; } } CreatureStats& stats = actor.getClass().getCreatureStats(actor); float actorWillpower = stats.getAttribute(ESM::Attribute::Willpower).getModified(); float actorLuck = stats.getAttribute(ESM::Attribute::Luck).getModified(); float castChance = (lowestSkill - calcSpellCost(*spell) + 0.2f * actorWillpower + 0.1f * actorLuck); return castChance; } 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); bool godmode = actor == getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); CreatureStats& stats = actor.getClass().getCreatureStats(actor); if (stats.getMagicEffects().getOrDefault(ESM::MagicEffect::Silence).getMagnitude() && !godmode) return 0; if (spell->mData.mType == ESM::Spell::ST_Power) return stats.getSpells().canUsePower(spell) ? 100 : 0; if (godmode) return 100; if (spell->mData.mType != ESM::Spell::ST_Spell) return 100; 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().getOrDefault(ESM::MagicEffect::Sound).getMagnitude(); float castChance = baseChance + castBonus; castChance *= stats.getFatigueTerm(); if (cap) return std::clamp(castChance, 0.f, 100.f); return std::max(castChance, 0.f); } float getSpellSuccessChance( const ESM::RefId& spellId, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool, bool cap, bool checkMagicka) { if (const auto spell = MWBase::Environment::get().getESMStore()->get().search(spellId)) return getSpellSuccessChance(spell, actor, effectiveSchool, cap, checkMagicka); return 0.f; } ESM::RefId getSpellSchool(const ESM::RefId& spellId, const MWWorld::Ptr& actor) { ESM::RefId school; getSpellSuccessChance(spellId, actor, &school); return school; } ESM::RefId getSpellSchool(const ESM::Spell* spell, const MWWorld::Ptr& actor) { ESM::RefId school; getSpellSuccessChance(spell, actor, &school); return school; } bool spellIncreasesSkill(const ESM::Spell* spell) { return spell->mData.mType == ESM::Spell::ST_Spell && !(spell->mData.mFlags & ESM::Spell::F_Always); } bool spellIncreasesSkill(const ESM::RefId& spellId) { const auto spell = MWBase::Environment::get().getESMStore()->get().search(spellId); return spell && spellIncreasesSkill(spell); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/spellutil.hpp000066400000000000000000000047021503074453300237170ustar00rootroot00000000000000#ifndef MWMECHANICS_SPELLUTIL_H #define MWMECHANICS_SPELLUTIL_H #include #include namespace ESM { struct EffectList; struct ENAMstruct; struct Enchantment; struct Ingredient; struct MagicEffect; struct Potion; struct Spell; } namespace MWWorld { class Ptr; } namespace MWMechanics { 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); 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 getPotionValue(const ESM::Potion& potion); std::optional rollIngredientEffect( MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0); /** * @param spell spell to cast * @param actor calculate spell success chance for this actor (depends on actor's skills) * @param effectiveSchool the spell's effective school (relevant for skill progress) will be written here * @param cap cap the result to 100%? * @param checkMagicka check magicka? * @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, 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); 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 ESM::RefId& spellId); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/stat.cpp000066400000000000000000000115701503074453300226510ustar00rootroot00000000000000#include "stat.hpp" #include #include namespace MWMechanics { template Stat::Stat() : mBase(0) , mModifier(0) { } template Stat::Stat(T base, T modified) : mBase(base) , mModifier(modified) { } template T Stat::getModified(bool capped) const { if (capped) return std::max({}, mModifier + mBase); return mModifier + mBase; } template void Stat::writeState(ESM::StatState& state) const { state.mBase = mBase; state.mMod = mModifier; } template void Stat::readState(const ESM::StatState& state) { mBase = state.mBase; mModifier = state.mMod; } template DynamicStat::DynamicStat() : mStatic(0, 0) , mCurrent(0) { } template DynamicStat::DynamicStat(T base) : mStatic(base, 0) , 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 void DynamicStat::setCurrent(const T& value, bool allowDecreaseBelowZero, bool allowIncreaseAboveModified) { if (value > mCurrent) { // increase if (value <= getModified() || allowIncreaseAboveModified) mCurrent = value; else if (mCurrent > getModified()) return; else mCurrent = getModified(); } else if (value > 0 || allowDecreaseBelowZero) { // allowed decrease mCurrent = value; } else if (mCurrent > 0) { // capped decrease mCurrent = 0; } } template T DynamicStat::getRatio(bool nanIsZero) const { T modified = getModified(); if (modified == T{}) { if (nanIsZero) return modified; return { 1 }; } return getCurrent() / modified; } template void DynamicStat::writeState(ESM::StatState& state) const { mStatic.writeState(state); state.mCurrent = mCurrent; } template void DynamicStat::readState(const ESM::StatState& state) { mStatic.readState(state); mCurrent = state.mCurrent; } AttributeValue::AttributeValue() : mBase(0.f) , mModifier(0.f) , mDamage(0.f) { } float AttributeValue::getModified() const { return std::max(0.f, mBase - mDamage + mModifier); } float AttributeValue::getBase() const { return mBase; } float AttributeValue::getModifier() const { return mModifier; } void AttributeValue::setBase(float base, bool clearModifier) { mBase = base; if (clearModifier) { mModifier = 0.f; mDamage = 0.f; } } void AttributeValue::setModifier(float mod) { if (mod < 0) { mModifier = 0.f; mDamage -= mod; } else mModifier = mod; } void AttributeValue::damage(float damage) { mDamage += damage; } void AttributeValue::restore(float amount) { if (mDamage <= 0) return; mDamage -= std::min(mDamage, amount); } float AttributeValue::getDamage() const { return mDamage; } void AttributeValue::writeState(ESM::StatState& state) const { state.mBase = mBase; state.mMod = mModifier; state.mDamage = mDamage; } void AttributeValue::readState(const ESM::StatState& state) { mBase = state.mBase; mModifier = state.mMod; mDamage = state.mDamage; } SkillValue::SkillValue() : mProgress(0) { } float SkillValue::getProgress() const { return mProgress; } void SkillValue::setProgress(float progress) { mProgress = progress; } void SkillValue::writeState(ESM::StatState& state) const { AttributeValue::writeState(state); state.mProgress = mProgress; } void SkillValue::readState(const ESM::StatState& state) { AttributeValue::readState(state); mProgress = state.mProgress; } } template class MWMechanics::Stat; template class MWMechanics::Stat; template class MWMechanics::DynamicStat; template class MWMechanics::DynamicStat; openmw-openmw-0.49.0/apps/openmw/mwmechanics/stat.hpp000066400000000000000000000110411503074453300226470ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_STAT_H #define GAME_MWMECHANICS_STAT_H namespace ESM { template struct StatState; } namespace MWMechanics { template class Stat { T mBase; T mModifier; public: typedef T Type; Stat(); Stat(T base, T modified); const T& getBase() const { return mBase; } T getModified(bool capped = true) const; T getModifier() const { return mModifier; } void setBase(const T& value) { mBase = value; } void setModifier(const T& modifier) { mModifier = modifier; } void writeState(ESM::StatState& state) const; void readState(const ESM::StatState& state); }; template inline bool operator==(const Stat& left, const Stat& right) { return left.getBase() == right.getBase() && left.getModifier() == right.getModifier(); } template inline bool operator!=(const Stat& left, const Stat& right) { return !(left == right); } template class DynamicStat { 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 { 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 base and adjust current accordingly. void setBase(const T& value) { mStatic.setBase(value); } void setCurrent(const T& value, bool allowDecreaseBelowZero = false, bool allowIncreaseAboveModified = 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); }; template inline bool operator==(const DynamicStat& left, const DynamicStat& right) { return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() && left.getCurrent() == right.getCurrent(); } template inline bool operator!=(const DynamicStat& left, const DynamicStat& right) { return !(left == right); } class AttributeValue { float mBase; float mModifier; float mDamage; // needs to be float to allow continuous damage public: AttributeValue(); float getModified() const; float getBase() const; float getModifier() const; void setBase(float base, bool clearModifier = false); void setModifier(float mod); // Maximum attribute damage is limited to the modified value. // 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); }; 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); }; inline bool operator==(const AttributeValue& left, const AttributeValue& right) { return left.getBase() == right.getBase() && left.getModifier() == right.getModifier() && left.getDamage() == right.getDamage(); } inline bool operator!=(const AttributeValue& left, const AttributeValue& right) { return !(left == 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(); } inline bool operator!=(const SkillValue& left, const SkillValue& right) { return !(left == right); } } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/steering.cpp000066400000000000000000000025441503074453300235170ustar00rootroot00000000000000#include "steering.hpp" #include #include #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwbase/environment.hpp" #include "movement.hpp" 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(); 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); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/steering.hpp000066400000000000000000000017621503074453300235250ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_STEERING_H #define OPENMW_MECHANICS_STEERING_H #include #include namespace MWWorld { class Ptr; } namespace MWMechanics { // 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)); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/summoning.cpp000066400000000000000000000161031503074453300237070ustar00rootroot00000000000000#include "summoning.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/manualref.hpp" #include "../mwrender/animation.hpp" #include "aifollow.hpp" #include "creaturestats.hpp" 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)); } static const std::map& getSummonMap() { 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" }, }; for (const auto& it : summonMapToGameSetting) { summonMap[it.first] = ESM::RefId::stringRefId( MWBase::Environment::get().getESMStore()->get().find(it.second)->mValue.getString()); } return summonMap; } ESM::RefId getSummonedCreature(int effectId) { const auto& summonMap = getSummonMap(); auto it = summonMap.find(effectId); if (it != summonMap.end()) { return it->second; } return ESM::RefId(); } int summonCreature(int effectId, const MWWorld::Ptr& summoner) { const ESM::RefId& creatureID = getSummonedCreature(effectId); int creatureActorId = -1; if (!creatureID.empty()) { try { 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) { const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); if (fx) anim->addEffect( Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(fx->mModel)).value(), "", 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 } 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(summoner, creature); if (!cleanup) return; for (auto it = creatureMap.begin(); it != creatureMap.end();) { 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()) { // Purge the magic effect so a new creature can be summoned if desired auto summon = *it; creatureMap.erase(it++); purgeSummonEffect(summoner, summon); } else ++it; } } void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon) { auto& creatureStats = summoner.getClass().getCreatureStats(summoner); 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); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/summoning.hpp000066400000000000000000000010451503074453300237130ustar00rootroot00000000000000#ifndef OPENMW_MECHANICS_SUMMONING_H #define OPENMW_MECHANICS_SUMMONING_H #include #include namespace ESM { class RefId; } namespace MWWorld { class Ptr; } namespace MWMechanics { bool isSummoningEffect(int effectId); ESM::RefId getSummonedCreature(int effectId); void purgeSummonEffect(const MWWorld::Ptr& summoner, const std::pair& summon); int summonCreature(int effectId, const MWWorld::Ptr& summoner); void updateSummons(const MWWorld::Ptr& summoner, bool cleanup); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/typedaipackage.hpp000066400000000000000000000015571503074453300246620ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_TYPEDAIPACKAGE_H #define GAME_MWMECHANICS_TYPEDAIPACKAGE_H #include "aipackage.hpp" namespace MWMechanics { template struct TypedAiPackage : public AiPackage { TypedAiPackage() : AiPackage(T::getTypeId(), T::makeDefaultOptions()) { } TypedAiPackage(bool repeat) : AiPackage(T::getTypeId(), T::makeDefaultOptions().withRepeat(repeat)) { } TypedAiPackage(const Options& options) : AiPackage(T::getTypeId(), options) { } template TypedAiPackage(Derived*) : AiPackage(Derived::getTypeId(), Derived::makeDefaultOptions()) { } std::unique_ptr clone() const override { return std::make_unique(*static_cast(this)); } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/weaponpriority.cpp000066400000000000000000000202161503074453300247660ustar00rootroot00000000000000#include "weaponpriority.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.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) { if (enemy.isEmpty() || item.getType() != ESM::Weapon::sRecordId) return 0.f; if (item.getClass().hasItemHealth(item) && item.getClass().getItemHealth(item) == 0) return 0.f; const ESM::Weapon* weapon = item.get()->mBase; if (type != -1 && weapon->mData.mType != type) return 0.f; const MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::Store& gmst = world->getStore().get(); ESM::WeaponType::Class weapclass = MWMechanics::getWeaponType(weapon->mData.mType)->mWeaponClass; if (type == -1 && weapclass == ESM::WeaponType::Ammo) return 0.f; float rating = 0.f; static const float fAIMeleeWeaponMult = gmst.find("fAIMeleeWeaponMult")->mValue.getFloat(); float ratingMult = fAIMeleeWeaponMult; if (weapclass != ESM::WeaponType::Melee) { // Underwater ranged combat is impossible if (world->isUnderwater(MWWorld::ConstPtr(actor), 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 if (getDistanceMinusHalfExtents(actor, enemy) >= getMaxAttackDistance(enemy)) { static const float fAIRangeMeleeWeaponMult = gmst.find("fAIRangeMeleeWeaponMult")->mValue.getFloat(); ratingMult = fAIRangeMeleeWeaponMult; } } const float chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1]) / 2.f; // We need to account for the fact that thrown weapons have 2x real damage applied to the target // as they're both the weapon and the ammo of the hit if (weapclass == ESM::WeaponType::Thrown) { rating = chop * 2; } else if (weapclass != ESM::WeaponType::Melee) { rating = chop; } else { const float slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1]) / 2.f; const float thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1]) / 2.f; rating = (slash * slash + thrust * thrust + chop * chop) / (slash + thrust + chop); } adjustWeaponDamage(rating, item, actor); if (weapclass != ESM::WeaponType::Ranged) { resistNormalWeapon(enemy, actor, item, rating); applyWerewolfDamageMult(enemy, item, rating); } else { int ammotype = MWMechanics::getWeaponType(weapon->mData.mType)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) { if (arrowRating <= 0.f) rating = 0.f; else rating += arrowRating; } else if (ammotype == ESM::Weapon::Bolt) { if (boltRating <= 0.f) rating = 0.f; else rating += boltRating; } } if (!weapon->mEnchant.empty()) { const ESM::Enchantment* enchantment = world->getStore().get().find(weapon->mEnchant); if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) { 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, false); } } int value = 50.f; 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(); value = ref->mBase->mData.mCombat; } // Take hit chance in account, but do not allow rating become negative. rating *= std::clamp(getHitChance(actor, enemy, value) / 100.f, 0.01f, 1.f); if (weapclass != ESM::WeaponType::Ammo) rating *= weapon->mData.mSpeed; return rating * ratingMult; } float rateAmmo(const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy, MWWorld::Ptr& bestAmmo, int ammoType) { float bestAmmoRating = 0.f; if (!actor.getClass().hasInventoryStore(actor)) return bestAmmoRating; MWWorld::InventoryStore& store = actor.getClass().getInventoryStore(actor); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { float rating = rateWeapon(*it, actor, enemy, ammoType); if (rating > bestAmmoRating) { bestAmmoRating = rating; bestAmmo = *it; } } return bestAmmoRating; } 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) { 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(); static const float fAIRangeMeleeWeaponMult = gmst.find("fAIRangeMeleeWeaponMult")->mValue.getFloat(); if (weapon.isEmpty()) return 0.f; float skillMult = actor.getClass().getSkill(actor, weapon.getClass().getEquipmentSkill(weapon)) * 0.01f; float chopMult = fAIMeleeWeaponMult; float bonusDamage = 0.f; const ESM::Weapon* esmWeap = weapon.get()->mBase; int type = esmWeap->mData.mType; if (getWeaponType(type)->mWeaponClass != ESM::WeaponType::Melee) { if (!ammo.isEmpty() && !MWBase::Environment::get().getWorld()->isSwimming(enemy)) { bonusDamage = ammo.get()->mBase->mData.mChop[1]; chopMult = fAIRangeMeleeWeaponMult; } else chopMult = 0.f; } float chopRating = (esmWeap->mData.mChop[1] + bonusDamage) * skillMult * chopMult; float slashRating = esmWeap->mData.mSlash[1] * skillMult * fAIMeleeWeaponMult; float thrustRating = esmWeap->mData.mThrust[1] * skillMult * fAIMeleeWeaponMult; return actor.getClass().getArmorRating(actor) * fAIMeleeArmorMult + std::max(std::max(chopRating, slashRating), thrustRating); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/weaponpriority.hpp000066400000000000000000000012341503074453300247720ustar00rootroot00000000000000#ifndef OPENMW_WEAPON_PRIORITY_H #define OPENMW_WEAPON_PRIORITY_H 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 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); } #endif openmw-openmw-0.49.0/apps/openmw/mwmechanics/weapontype.cpp000066400000000000000000000404121503074453300240660ustar00rootroot00000000000000#include "weapontype.hpp" #include "creaturestats.hpp" #include "drawstate.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include #include namespace MWMechanics { 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 { 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) { MWWorld::ContainerStoreIterator weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (weapon == inv.end()) *weaptype = ESM::Weapon::HandToHand; else { auto type = weapon->getType(); if (type == ESM::Weapon::sRecordId) { const MWWorld::LiveCellRef* ref = weapon->get(); *weaptype = ref->mBase->mData.mType; } else if (type == ESM::Lockpick::sRecordId || type == ESM::Probe::sRecordId) *weaptype = ESM::Weapon::PickProbe; } return weapon; } return inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight); } const ESM::WeaponType* getWeaponType(const int weaponType) { 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++) { std::string_view shortGroup = getWeaponType(type)->mShortGroup; if (!shortGroup.empty()) shortGroupSet.insert(shortGroup); } return std::vector(shortGroupSet.begin(), shortGroupSet.end()); } } openmw-openmw-0.49.0/apps/openmw/mwmechanics/weapontype.hpp000066400000000000000000000011241503074453300240700ustar00rootroot00000000000000#ifndef GAME_MWMECHANICS_WEAPONTYPE_H #define GAME_MWMECHANICS_WEAPONTYPE_H #include #include namespace ESM { struct WeaponType; } namespace MWWorld { class Ptr; template class ContainerStoreIteratorBase; 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 openmw-openmw-0.49.0/apps/openmw/mwphysics/000077500000000000000000000000001503074453300207165ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwphysics/actor.cpp000066400000000000000000000250231503074453300225340ustar00rootroot00000000000000#include "actor.hpp" #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, 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) { // 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) { 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() << "\"."; } 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; } mConvexShape = static_cast(mShape.get()); mConvexShape->setMargin(0.001); // make sure bullet isn't using the huge default convex shape margin of 0.04 mCollisionObject = std::make_unique(); mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); updateScaleUnsafe(); if (!mRotationallyInvariant) { const SceneUtil::PositionAttitudeTransform* baseNode = mPtr.getRefData().getBaseNode(); if (baseNode) mRotation = baseNode->getAttitude(); } addCollisionMask(getCollisionMask()); updateCollisionObjectPositionUnsafe(); } Actor::~Actor() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } void Actor::enableCollisionMode(bool collision) { mInternalCollisionMode = collision; } void Actor::enableCollisionBody(bool collision) { if (mExternalCollisionMode != collision) { mExternalCollisionMode = collision; updateCollisionMask(); } } void Actor::addCollisionMask(int collisionMask) { mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Actor, collisionMask); } void Actor::updateCollisionMask() { mTaskScheduler->setCollisionFilterMask(mCollisionObject.get(), getCollisionMask()); } 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::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; } void Actor::setSimulationPosition(const osg::Vec3f& position) { if (!std::exchange(mSkipSimulation, false)) mSimulationPosition = position; } osg::Vec3f Actor::getScaledMeshTranslation() const { return mRotation * osg::componentMultiply(mMeshTranslation, mScale); } void Actor::updateCollisionObjectPosition() { std::scoped_lock lock(mPositionMutex); updateCollisionObjectPositionUnsafe(); } void Actor::updateCollisionObjectPositionUnsafe() { mShape->setLocalScaling(Misc::Convert::toBullet(mScale)); osg::Vec3f newPosition = getScaledMeshTranslation() + mPosition; auto& trans = mCollisionObject->getWorldTransform(); trans.setOrigin(Misc::Convert::toBullet(newPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); mCollisionObject->setWorldTransform(trans); } osg::Vec3f Actor::getCollisionObjectPosition() const { std::scoped_lock lock(mPositionMutex); return getScaledMeshTranslation() + 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; } void Actor::adjustPosition(const osg::Vec3f& offset) { std::scoped_lock lock(mPositionMutex); mPositionOffset += offset; } osg::Vec3f Actor::applyOffsetChange() { if (mPositionOffset.length2() != 0) { mPosition += mPositionOffset; mPreviousPosition += mPositionOffset; mSimulationPosition += mPositionOffset; mPositionOffset = osg::Vec3f(); } return mPosition; } void Actor::setRotation(osg::Quat quat) { std::scoped_lock lock(mPositionMutex); mRotation = quat; } bool Actor::isRotationallyInvariant() const { return mRotationallyInvariant; } void Actor::updateScale() { std::scoped_lock lock(mPositionMutex); updateScaleUnsafe(); } void Actor::updateScaleUnsafe() { float scale = mPtr.getCellRef().getScale(); osg::Vec3f scaleVec(scale, scale, scale); mPtr.getClass().adjustScale(mPtr, scaleVec, false); mScale = scaleVec; mHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); scaleVec = osg::Vec3f(scale, scale, scale); mPtr.getClass().adjustScale(mPtr, scaleVec, true); mRenderingHalfExtents = osg::componentMultiply(mOriginalHalfExtents, scaleVec); } osg::Vec3f Actor::getHalfExtents() const { return mHalfExtents; } osg::Vec3f Actor::getOriginalHalfExtents() const { return mOriginalHalfExtents; } osg::Vec3f Actor::getRenderingHalfExtents() const { return mRenderingHalfExtents; } void Actor::setInertialForce(const osg::Vec3f& force) { mForce = force; } void Actor::setOnGround(bool grounded) { mOnGround = grounded; } void Actor::setOnSlope(bool slope) { mOnSlope = slope; } bool Actor::isWalkingOnWater() const { return mWalkingOnWater; } void Actor::setWalkingOnWater(bool walkingOnWater) { mWalkingOnWater = walkingOnWater; } void Actor::setCanWaterWalk(bool waterWalk) { if (waterWalk != mCanWaterWalk) { mCanWaterWalk = waterWalk; updateCollisionMask(); } } MWWorld::Ptr Actor::getStandingOnPtr() const { std::scoped_lock lock(mPositionMutex); return mStandingOnPtr; } void Actor::setStandingOnPtr(const MWWorld::Ptr& ptr) { std::scoped_lock lock(mPositionMutex); mStandingOnPtr = ptr; } 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); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/actor.hpp000066400000000000000000000144771503074453300225540ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_ACTOR_H #define OPENMW_MWPHYSICS_ACTOR_H #include #include #include "ptrholder.hpp" #include #include #include class btCollisionShape; class btCollisionWorld; class btConvexShape; namespace Resource { struct BulletShape; } namespace MWPhysics { class PhysicsTaskScheduler; class Actor final : public PtrHolder { public: 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; } btConvexShape* getConvexShape() const { return mConvexShape; } /** * Enables or disables the *external* collision body. If disabled, other actors will not collide with this * actor. */ void enableCollisionBody(bool collision); void updateScale(); void setRotation(osg::Quat quat); /** * Return true if the collision shape looks the same no matter how its Z rotated. */ 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 */ void setSimulationPosition(const osg::Vec3f& position); void updateCollisionObjectPosition(); /** * Returns the half extents of the collision body (scaled according to collision scale) */ osg::Vec3f getHalfExtents() const; /** * Returns the half extents of the collision body (not scaled) */ osg::Vec3f getOriginalHalfExtents() 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. */ osg::Vec3f getCollisionObjectPosition() const; /** * 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); // apply position offset. Can't be called during simulation 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. */ osg::Vec3f getRenderingHalfExtents() const; /** * Sets the current amount of inertial force (incl. gravity) affecting this physic actor */ 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; } void setOnGround(bool grounded); bool getOnGround() const { return mOnGround; } void setOnSlope(bool slope); bool getOnSlope() const { return mOnSlope; } /// Sets whether this actor should be able to collide with the water surface void setCanWaterWalk(bool waterWalk); /// Sets whether this actor has been walking on the water surface in the last frame void setWalkingOnWater(bool walkingOnWater); bool isWalkingOnWater() const; 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 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; /// Removes then re-adds the collision object to the dynamics world void updateCollisionMask(); void addCollisionMask(int collisionMask); int getCollisionMask() const; /// Returns the mesh translation, scaled and rotated as necessary osg::Vec3f getScaledMeshTranslation() const; bool mCanWaterWalk; bool mWalkingOnWater; bool mRotationallyInvariant; DetourNavigator::CollisionShapeType mCollisionShapeType; std::unique_ptr mShape; btConvexShape* mConvexShape; osg::Vec3f mMeshTranslation; osg::Vec3f mOriginalHalfExtents; osg::Vec3f mHalfExtents; osg::Vec3f mRenderingHalfExtents; osg::Quat mRotation; osg::Vec3f mScale; osg::Vec3f mPositionOffset; bool mSkipSimulation = true; mutable std::mutex mPositionMutex; unsigned int mStuckFrames; osg::Vec3f mLastStuckPosition; osg::Vec3f mForce; bool mOnGround; bool mOnSlope; bool mInternalCollisionMode; bool mExternalCollisionMode; bool mActive; PhysicsTaskScheduler* mTaskScheduler; inline void updateScaleUnsafe(); inline void updateCollisionObjectPositionUnsafe(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/actorconvexcallback.cpp000066400000000000000000000106111503074453300254310ustar00rootroot00000000000000#include "actorconvexcallback.hpp" #include "collisiontype.hpp" #include "contacttestwrapper.h" #include #include #include "projectile.hpp" namespace MWPhysics { namespace { struct ActorOverlapTester : public btCollisionWorld::ContactResultCallback { bool mOverlapping = false; 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) { if (convexResult.m_hitCollisionObject == mMe) 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) { 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); 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); 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) { convexResult.m_hitFraction = 0.0f; convexResult.m_hitNormalLocal = Misc::Convert::toBullet(normal); return ClosestConvexResultCallback::addSingleResult(convexResult, true); } else { return 1; } } } if (convexResult.m_hitCollisionObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Projectile) { auto* projectileHolder = static_cast(convexResult.m_hitCollisionObject->getUserPointer()); if (!projectileHolder->isActive()) return 1; if (projectileHolder->isValidTarget(mMe)) projectileHolder->hit(mMe, convexResult.m_hitPointLocal, convexResult.m_hitNormalLocal); return 1; } btVector3 hitNormalWorld; if (normalInWorldSpace) hitNormalWorld = convexResult.m_hitNormalLocal; else { /// 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 1; return ClosestConvexResultCallback::addSingleResult(convexResult, normalInWorldSpace); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/actorconvexcallback.hpp000066400000000000000000000020141503074453300254340ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H #define OPENMW_MWPHYSICS_ACTORCONVEXCALLBACK_H #include class btCollisionObject; namespace MWPhysics { class ActorConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: 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; protected: const btCollisionObject* mMe; const btVector3 mMotion; const btScalar mMinCollisionDot; const btCollisionWorld* mWorld; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/closestnotmerayresultcallback.cpp000066400000000000000000000015621503074453300275750ustar00rootroot00000000000000#include "closestnotmerayresultcallback.hpp" #include #include #include "collisiontype.hpp" namespace MWPhysics { btScalar ClosestNotMeRayResultCallback::addSingleResult( btCollisionWorld::LocalRayResult& rayResult, bool normalInWorldSpace) { const auto* hitObject = rayResult.m_collisionObject; if (std::find(mIgnoreList.begin(), mIgnoreList.end(), hitObject) != mIgnoreList.end()) return 1.f; if (hitObject->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor && !mTargets.empty()) { if ((std::find(mTargets.begin(), mTargets.end(), hitObject) == mTargets.end())) return 1.f; } return btCollisionWorld::ClosestRayResultCallback::addSingleResult(rayResult, normalInWorldSpace); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/closestnotmerayresultcallback.hpp000066400000000000000000000017371503074453300276060ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CLOSESTNOTMERAYRESULTCALLBACK_H #include #include class btCollisionObject; namespace MWPhysics { class Projectile; class ClosestNotMeRayResultCallback : public btCollisionWorld::ClosestRayResultCallback { public: 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 std::span mIgnoreList; const std::span mTargets; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/collisiontype.hpp000066400000000000000000000014271503074453300243300ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_COLLISIONTYPE_H #define OPENMW_MWPHYSICS_COLLISIONTYPE_H 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, 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 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/constants.hpp000066400000000000000000000023331503074453300234440ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_CONSTANTS_H #define OPENMW_MWPHYSICS_CONSTANTS_H namespace MWPhysics { 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 static constexpr bool sDoExtraStairHacks = true; static constexpr float sGroundOffset = 1.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.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; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/contacttestresultcallback.cpp000066400000000000000000000016441503074453300266760ustar00rootroot00000000000000#include "contacttestresultcallback.hpp" #include #include "components/misc/convert.hpp" #include "ptrholder.hpp" namespace MWPhysics { 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) }); return 0.f; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/contacttestresultcallback.hpp000066400000000000000000000015671503074453300267070ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_CONTACTTESTRESULTCALLBACK_H #define OPENMW_MWPHYSICS_CONTACTTESTRESULTCALLBACK_H #include #include #include "physicssystem.hpp" class btCollisionObject; struct btCollisionObjectWrapper; namespace MWPhysics { class ContactTestResultCallback : public btCollisionWorld::ContactResultCallback { public: 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; std::vector mResult; private: const btCollisionObject* mTestedAgainst; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/contacttestwrapper.cpp000066400000000000000000000014631503074453300253620ustar00rootroot00000000000000#include #include "contacttestwrapper.h" 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) { std::unique_lock lock(contactMutex); collisionWorld->contactTest(colObj, resultCallback); } void ContactTestWrapper::contactPairTest(btCollisionWorld* collisionWorld, btCollisionObject* colObjA, btCollisionObject* colObjB, btCollisionWorld::ContactResultCallback& resultCallback) { std::unique_lock lock(contactMutex); collisionWorld->contactPairTest(colObjA, colObjB, resultCallback); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/contacttestwrapper.h000066400000000000000000000011171503074453300250230ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H #define OPENMW_MWPHYSICS_CONTACTTESTWRAPPER_H #include 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/hasspherecollisioncallback.hpp000066400000000000000000000051071503074453300270050ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #define OPENMW_MWPHYSICS_HASSPHERECOLLISIONCALLBACK_H #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) { 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, 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 && mOnCollision == nullptr) return false; const auto collisionObject = static_cast(proxy->m_clientObject); if (mIgnore(collisionObject) || !needsCollision(*proxy) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) return true; mResult = true; if (mOnCollision != nullptr) { (*mOnCollision)(collisionObject); return true; } return !mResult; } bool getResult() const { return mResult; } private: btVector3 mPosition; btScalar mRadius; Ignore mIgnore; int mCollisionFilterMask; int mCollisionFilterGroup; OnCollision* mOnCollision; bool mResult = false; bool needsCollision(const btBroadphaseProxy& proxy) const { bool collides = (proxy.m_collisionFilterGroup & mCollisionFilterMask) != 0; collides = collides && (mCollisionFilterGroup & proxy.m_collisionFilterMask); return collides; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/heightfield.cpp000066400000000000000000000070731503074453300237050ustar00rootroot00000000000000#include "heightfield.hpp" #include "mtphysics.hpp" #include #include #include #include #include #include #if BT_BULLET_VERSION < 310 // Older Bullet versions only support `btScalar` heightfields. // Our heightfield data is `float`. // // These functions handle conversion from `float` to `double` when // `btScalar` is `double` (`BT_USE_DOUBLE_PRECISION`). namespace { template auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { return {}; } template auto makeHeights(const T* heights, int verts) -> std::enable_if_t::value, std::vector> { return std::vector(heights, heights + static_cast(verts * verts)); } template auto getHeights(const T* floatHeights, const std::vector&) -> std::enable_if_t::value, const btScalar*> { return floatHeights; } template auto getHeights(const T*, const std::vector& btScalarHeights) -> std::enable_if_t::value, const btScalar*> { return btScalarHeights.data(); } } #endif namespace MWPhysics { 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, verts)) #endif , mTaskScheduler(scheduler) { #if BT_BULLET_VERSION < 310 mShape = std::make_unique( verts, verts, getHeights(heights, mHeights), 1, minH, maxH, 2, PHY_FLOAT, false); #else mShape = std::make_unique(verts, verts, heights, minH, maxH, 2, false); #endif mShape->setUseDiamondSubdivision(true); 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. // // Note: The accelerator data structure in Bullet is only used // in some operations. This could be improved, see: // https://github.com/bulletphysics/bullet3/issues/3276 mShape->buildAccelerator(); #endif 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); } HeightField::~HeightField() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } btCollisionObject* HeightField::getCollisionObject() { return mCollisionObject.get(); } const btCollisionObject* HeightField::getCollisionObject() const { return mCollisionObject.get(); } const btHeightfieldTerrainShape* HeightField::getShape() const { return mShape.get(); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/heightfield.hpp000066400000000000000000000021751503074453300237100ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_HEIGHTFIELD_H #define OPENMW_MWPHYSICS_HEIGHTFIELD_H #include #include #include #include class btCollisionObject; class btHeightfieldTerrainShape; namespace osg { class Object; } namespace MWPhysics { class PhysicsTaskScheduler; class HeightField { public: 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(); const btCollisionObject* getCollisionObject() const; const btHeightfieldTerrainShape* getShape() const; private: std::unique_ptr mShape; std::unique_ptr mCollisionObject; osg::ref_ptr mHoldObject; #if BT_BULLET_VERSION < 310 std::vector mHeights; #endif PhysicsTaskScheduler* mTaskScheduler; void operator=(const HeightField&); HeightField(const HeightField&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/movementsolver.cpp000066400000000000000000000655501503074453300245220ustar00rootroot00000000000000#include "movementsolver.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.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" #include namespace MWPhysics { static bool isActor(const btCollisionObject* obj) { assert(obj); return obj->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Actor; } namespace { class ContactCollectionCallback : public btCollisionWorld::ContactResultCallback { public: explicit ContactCollectionCallback(const btCollisionObject& me, const osg::Vec3f& velocity) : mVelocity(Misc::Convert::toBullet(velocity)) { m_collisionFilterGroup = me.getBroadphaseHandle()->m_collisionFilterGroup; m_collisionFilterMask = me.getBroadphaseHandle()->m_collisionFilterMask & ~CollisionType_Projectile; } 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) { 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; }; } 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); if (tracer.mFraction >= 1.0f) { actor->setOnGround(false); return position; } actor->setOnGround(true); // Check if we actually found a valid spawn point (use an infinitely thin ray this time). // 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); btCollisionWorld::ClosestRayResultCallback resultCallback1(from, to); 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))) { actor->setOnSlope(!isWalkableSlope(resultCallback1.m_hitNormalWorld)); return Misc::Convert::toOsg(resultCallback1.m_hitPointWorld) + osg::Vec3f(0.f, 0.f, sGroundOffset); } actor->setOnSlope(!isWalkableSlope(tracer.mPlaneNormal)); return tracer.mEndPos - offset + osg::Vec3f(0.f, 0.f, sGroundOffset); } void MovementSolver::move( ActorFrameData& actor, float time, const btCollisionWorld* collisionWorld, const WorldFrameData& worldData) { // Reset per-frame data actor.mWalkingOnWater = false; // Anything to collide with? if (actor.mSkipCollisionDetection) { 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; } // 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() actor.mPosition.z() += actor.mHalfExtentsZ; // vanilla-accurate float swimlevel = actor.mSwimLevel + actor.mHalfExtentsZ; ActorTracer tracer; osg::Vec3f velocity; // 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(actor.mRotation.x(), osg::Vec3f(-1, 0, 0)) * osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; } else { velocity = (osg::Quat(actor.mRotation.y(), osg::Vec3f(0, 0, -1))) * actor.mMovement; if ((velocity.z() > 0.f && actor.mIsOnGround && !actor.mIsOnSlope) || (velocity.z() > 0.f && velocity.z() + actor.mInertia.z() <= -velocity.z() && actor.mIsOnSlope)) actor.mInertia = velocity; else if (!actor.mIsOnGround || actor.mIsOnSlope) velocity = velocity + actor.mInertia; } // Now that we have the effective movement vector, apply wind forces to it if (worldData.mIsInStorm && velocity.length() > 0) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const float fStromWalkMult = store.get().find("fStromWalkMult")->mValue.getFloat(); const float angleCos = worldData.mStormDirection * velocity / velocity.length(); velocity *= 1.f + fStromWalkMult * angleCos; } 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; int numTimesSlid = 0; 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 && underwater) { 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) { // trace to where character would go if there were no obstructions tracer.doTrace(actor.mCollisionObject, newPosition, nextpos, collisionWorld, actor.mIsOnGround); // check for obstructions if (tracer.mFraction >= 1.0f) { newPosition = tracer.mEndPos; // ok to move, so set newPosition break; } } else { // The current position and next position are nearly the same, so just exit. // Note: Bullet can trigger an assert in debug modes if the positions // are the same, since that causes it to attempt to normalize a zero // length vector (which can also happen with nearly identical vectors, since // precision can be lost due to any math Bullet does internally). Since we // aren't performing any collision detection, we want to reject the next // position, so that we don't slowly move inside another object. break; } 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() + actor.mHalfExtentsZ; osg::Vec3f oldPosition = newPosition; bool usedStepLogic = false; if (!isActor(tracer.mHitObject)) { 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) { if (actor.mIsAquatic && newPosition.z() + actor.mHalfExtentsZ > actor.mWaterlevel) newPosition = oldPosition; 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); 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) if (seenGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) { planeNormal.z() = 0; planeNormal.normalize(); } // Move up to what we ran into (with a bit of a collision margin) if ((newPosition - tracer.mEndPos).length2() > sCollisionMargin * sCollisionMargin) { auto direction = velocity; direction.normalize(); newPosition = tracer.mEndPos; 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 for this, we want to use the original plane normal, or else certain types of geometry will // snag if (numTimesSlid > 0) { 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) { 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) { bestNormal = lastSlideNormalFallback; lastSlideNormal = lastSlideNormalFallback; } 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 + origPlaneNormal; averageNormal.normalize(); tracer.doTrace(actor.mCollisionObject, newPosition, newPosition + averageNormal * (sCollisionMargin * 2.0), collisionWorld); newPosition = (newPosition + tracer.mEndPos) / 2.0; usedSeamLogic = true; } } } // otherwise just keep the normal vector rejection // 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) { 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. // 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()); numTimesSlid += 1; lastSlideNormalFallback = lastSlideNormal; lastSlideNormal = origPlaneNormal; velocity = newVelocity; } } bool isOnGround = false; bool isOnSlope = false; if (forceGroundTest || (actor.mInertia.z() <= 0.f && newPosition.z() >= swimlevel)) { osg::Vec3f from = newPosition; auto dropDistance = 2 * sGroundOffset + (actor.mIsOnGround ? sStepSizeDown : 0); osg::Vec3f to = newPosition - osg::Vec3f(0, 0, dropDistance); tracer.doTrace(actor.mCollisionObject, from, to, collisionWorld, actor.mIsOnGround); if (tracer.mFraction < 1.0f) { if (!isActor(tracer.mHitObject)) { isOnGround = true; isOnSlope = !isWalkableSlope(tracer.mPlaneNormal); actor.mStandingOn = tracer.mHitObject; if (actor.mStandingOn->getBroadphaseHandle()->m_collisionFilterGroup == CollisionType_Water) actor.mWalkingOnWater = true; if (!actor.mFlying && !isOnSlope) { if (tracer.mFraction * dropDistance > sGroundOffset) newPosition.z() = tracer.mEndPos.z() + sGroundOffset; else { newPosition.z() = tracer.mEndPos.z(); 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()) 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 (actor.mStuckFrames > 0) { isOnGround = true; isOnSlope = false; } } if ((isOnGround && !isOnSlope) || newPosition.z() < swimlevel || actor.mFlying) actor.mInertia = osg::Vec3f(0.f, 0.f, 0.f); else { 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; } } 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() -= 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) return delta; return delta + delta.normalized() * sCollisionMargin; } void MovementSolver::unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld) { if (actor.mSkipCollisionDetection) // noclipping/tcl return; if (actor.mMovement.length2() == 0) // no AI nor player attempted to move, current position is assumed correct return; auto tempPosition = actor.mPosition; if (actor.mStuckFrames >= 10) { if ((actor.mLastStuckPosition - actor.mPosition).length2() < 100) return; else { 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, actor.mHalfExtentsZ); // use a 3d approximation of the movement vector to better judge player intent 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 (!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 = actor.mCollisionObject->getWorldTransform(); btTransform newTransform = oldTransform; auto gatherContacts = [&](btVector3 newOffset) -> ContactCollectionCallback { goodPosition = refPosition + Misc::Convert::toOsg(addMarginToDelta(newOffset)); newTransform.setOrigin(Misc::Convert::toBullet(goodPosition)); actor.mCollisionObject->setWorldTransform(newTransform); 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) { ++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) positionDelta *= contactCallback.mMaxX / std::abs(positionDelta.x()); if (std::abs(positionDelta.y()) > contactCallback.mMaxY) positionDelta *= contactCallback.mMaxY / std::abs(positionDelta.y()); 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) 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()) }); // success if (contactCallback3.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; else // try again but fixed distance up { auto contactCallback4 = gatherContacts({ 0.0, 0.0, 10.0 }); // success if (contactCallback4.mDistance > contactCallback.mDistance) tempPosition = goodPosition - verticalHalfExtent; } } } else { actor.mStuckFrames = 0; actor.mLastStuckPosition = { 0, 0, 0 }; } actor.mCollisionObject->setWorldTransform(oldTransform); actor.mPosition = tempPosition; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/movementsolver.hpp000066400000000000000000000027171503074453300245230ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_MOVEMENTSOLVER_H #define OPENMW_MWPHYSICS_MOVEMENTSOLVER_H #include #include class btCollisionWorld; namespace MWWorld { class Ptr; } namespace MWPhysics { /// Vector projection 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) { return direction - project(direction, planeNormal); } template static bool isWalkableSlope(const Vec3& normal) { 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, const WorldFrameData& worldData); static void move(ProjectileFrameData& projectile, float time, const btCollisionWorld* collisionWorld); static void unstuck(ActorFrameData& actor, const btCollisionWorld* collisionWorld); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/mtphysics.cpp000066400000000000000000001035131503074453300234500ustar00rootroot00000000000000#include "mtphysics.hpp" #include #include #include #include #include #include #include #include #include #include #include #include "components/debug/debuglog.hpp" #include "components/misc/convert.hpp" #include #include #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "actor.hpp" #include "contacttestwrapper.h" #include "movementsolver.hpp" #include "object.hpp" #include "physicssystem.hpp" #include "projectile.hpp" namespace MWPhysics { 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 mutex /// @param threadCount decide wether the excluse lock will be taken explicit MaybeExclusiveLock(Mutex& mutex, LockingPolicy lockingPolicy) : mImpl(makeExclusiveLock(mutex, lockingPolicy)) { } private: std::optional> mImpl; }; template std::optional> makeSharedLock(Mutex& mutex, LockingPolicy lockingPolicy) { if (lockingPolicy == LockingPolicy::NoLocks) return {}; return std::shared_lock(mutex); } /// @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)) { } private: std::optional> mImpl; }; template std::variant, std::shared_lock> makeLock( Mutex& mutex, LockingPolicy lockingPolicy) { 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))); } /// @brief A scoped lock that is either shared, exclusive or inexistent depending on configuration template class MaybeLock { 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; }; } } namespace { bool isUnderWater(const MWPhysics::ActorFrameData& actorData) { return actorData.mPosition.z() < actorData.mSwimLevel; } osg::Vec3f interpolateMovements(const MWPhysics::PtrHolder& ptr, float timeAccum, float physicsDt) { const float interpolationFactor = std::clamp(timeAccum / physicsDt, 0.0f, 1.0f); return ptr.getPosition() * interpolationFactor + ptr.getPreviousPosition() * (1.f - interpolationFactor); } using LockedActorSimulation = std::pair, std::reference_wrapper>; using LockedProjectileSimulation = std::pair, std::reference_wrapper>; namespace Visitors { template class Lock> struct WithLockedPtr { const Impl& mImpl; std::shared_mutex& mCollisionWorldMutex; const MWPhysics::LockingPolicy mLockingPolicy; template void operator()(MWPhysics::SimulationImpl& sim) const { 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); } }; 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 { 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) { Log(Debug::Info) << "Using " << mNumThreads << " async physics threads"; for (unsigned i = 0; i < mNumThreads; ++i) mThreads.emplace_back([&] { worker(); }); } else { mLOSCacheExpiry = 0; } mPreStepBarrier = std::make_unique(mNumThreads); mPostStepBarrier = std::make_unique(mNumThreads); mPostSimBarrier = std::make_unique(mNumThreads); } PhysicsTaskScheduler::~PhysicsTaskScheduler() { waitForWorkers(); { MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); mNumJobs = 0; mRemainingSteps = 0; } if (mWorkersSync != nullptr) mWorkersSync->stopWorkers(); for (auto& thread : mThreads) thread.join(); } std::tuple PhysicsTaskScheduler::calculateStepConfig(float timeAccum) const { int maxAllowedSteps = 2; int numSteps = timeAccum / mDefaultPhysicsDt; // 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 // get physics timing stats float budgetMeasurement = std::max(mBudget.get(), mAsyncBudget.get()); // time spent per step in terms of the intended physics framerate budgetMeasurement /= mDefaultPhysicsDt; // ensure sane minimum value budgetMeasurement = std::max(0.00001f, budgetMeasurement); // we're spending almost or more than realtime per physics frame; limit to a single step if (budgetMeasurement > 0.95) maxAllowedSteps = 1; // physics is fairly cheap; limit based on expense if (budgetMeasurement < 0.5) maxAllowedSteps = std::ceil(1.0 / budgetMeasurement); // limit to a reasonable amount maxAllowedSteps = std::min(10, maxAllowedSteps); // fall back to delta time for this frame if fixed timestep physics would fall behind float actualDelta = mDefaultPhysicsDt; if (numSteps > maxAllowedSteps) { numSteps = maxAllowedSteps; // 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); // 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) actualDelta = std::max(actualDelta, mDefaultPhysicsDt); } return std::make_tuple(numSteps, actualDelta); } 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. MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); double timeStart = mTimer->tick(); // start by finishing previous background computation if (mNumThreads != 0) { syncWithMainThread(); if (mAdvanceSimulation) mAsyncBudget.update(mTimer->delta_s(mAsyncStartTime, mTimeEnd), mPrevStepCount, mBudgetCursor); updateStats(frameStart, frameNumber, stats); } auto [numSteps, newDelta] = calculateStepConfig(timeAccum); timeAccum -= numSteps * newDelta; // init const Visitors::InitPosition vis{ mCollisionWorld }; for (auto& sim : simulations) { std::visit(vis, sim); } mPrevStepCount = numSteps; mRemainingSteps = numSteps; mTimeAccum = timeAccum; mPhysicsDt = newDelta; mSimulations = &simulations; mAdvanceSimulation = (mRemainingSteps != 0); mNumJobs = mSimulations->size(); mNextLOS.store(0, std::memory_order_relaxed); mNextJob.store(0, std::memory_order_release); if (mAdvanceSimulation) mWorldFrameData = std::make_unique(); if (mAdvanceSimulation) mBudgetCursor += 1; if (mNumThreads == 0) { doSimulation(); syncWithMainThread(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), numSteps, mBudgetCursor); return; } mAsyncStartTime = mTimer->tick(); if (mAdvanceSimulation) mBudget.update(mTimer->delta_s(timeStart, mTimer->tick()), 1, mBudgetCursor); } void PhysicsTaskScheduler::resetSimulation(const ActorMap& actors) { waitForWorkers(); MaybeExclusiveLock lock(mSimulationMutex, mLockingPolicy); mBudget.reset(mDefaultPhysicsDt); mAsyncBudget.reset(0.0f); if (mSimulations != nullptr) { mSimulations->clear(); mSimulations = nullptr; } for (const auto& [_, actor] : actors) { actor->updatePosition(); actor->updateCollisionObjectPosition(); } } void PhysicsTaskScheduler::rayTest(const btVector3& rayFromWorld, const btVector3& rayToWorld, btCollisionWorld::RayResultCallback& resultCallback) const { 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 { MaybeLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->convexSweepTest(castShape, from, to, resultCallback); } void PhysicsTaskScheduler::contactTest( btCollisionObject* colObj, btCollisionWorld::ContactResultCallback& resultCallback) { MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); ContactTestWrapper::contactTest(mCollisionWorld, colObj, resultCallback); } std::optional PhysicsTaskScheduler::getHitPoint(const btTransform& from, btCollisionObject* target) { MaybeLock lock(mCollisionWorldMutex, mLockingPolicy); // target the collision object's world origin, this should be the center of the collision object btTransform rayTo; rayTo.setIdentity(); rayTo.setOrigin(target->getWorldTransform().getOrigin()); btCollisionWorld::ClosestRayResultCallback cb(from.getOrigin(), rayTo.getOrigin()); 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 }; } void PhysicsTaskScheduler::aabbTest( const btVector3& aabbMin, const btVector3& aabbMax, btBroadphaseAabbCallback& callback) { MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->getBroadphase()->aabbTest(aabbMin, aabbMax, callback); } void PhysicsTaskScheduler::getAabb(const btCollisionObject* obj, btVector3& min, btVector3& max) { MaybeSharedLock lock(mCollisionWorldMutex, mLockingPolicy); obj->getCollisionShape()->getAabb(obj->getWorldTransform(), min, max); } void PhysicsTaskScheduler::setCollisionFilterMask(btCollisionObject* collisionObject, int collisionFilterMask) { MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); collisionObject->getBroadphaseHandle()->m_collisionFilterMask = collisionFilterMask; } void PhysicsTaskScheduler::addCollisionObject( btCollisionObject* collisionObject, int collisionFilterGroup, int collisionFilterMask) { MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionObjects.insert(collisionObject); mCollisionWorld->addCollisionObject(collisionObject, collisionFilterGroup, collisionFilterMask); } void PhysicsTaskScheduler::removeCollisionObject(btCollisionObject* collisionObject) { MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); mCollisionObjects.erase(collisionObject); mCollisionWorld->removeCollisionObject(collisionObject); } void PhysicsTaskScheduler::updateSingleAabb(const std::shared_ptr& ptr, bool immediate) { if (immediate || mNumThreads == 0) { updatePtrAabb(ptr); } else { MaybeExclusiveLock lock(mUpdateAabbMutex, mLockingPolicy); mUpdateAabb.insert(ptr); } } bool PhysicsTaskScheduler::getLineOfSight( const std::shared_ptr& actor1, const std::shared_ptr& actor2) { 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(actor1.get(), actor2.get()); mLOSCache.push_back(req); return req.mResult; } result->mAge = 0; return result->mResult; } void PhysicsTaskScheduler::refreshLOSCache() { MaybeSharedLock lock(mLOSCacheMutex, mLockingPolicy); int job = 0; int numLOS = mLOSCache.size(); while ((job = mNextLOS.fetch_add(1, std::memory_order_relaxed)) < numLOS) { auto& req = mLOSCache[job]; auto actorPtr1 = req.mActors[0].lock(); auto actorPtr2 = req.mActors[1].lock(); if (req.mAge++ > mLOSCacheExpiry || !actorPtr1 || !actorPtr2) req.mStale = true; else req.mResult = hasLineOfSight(actorPtr1.get(), actorPtr2.get()); } } void PhysicsTaskScheduler::updateAabbs() { 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::shared_ptr& ptr) { MaybeExclusiveLock lock(mCollisionWorldMutex, mLockingPolicy); if (const auto actor = std::dynamic_pointer_cast(ptr)) { 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() { mWorkersSync->runWorker([this] { std::shared_lock lock(mSimulationMutex); doSimulation(); }); } void PhysicsTaskScheduler::updateActorsPositions() { 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)); btCollisionWorld::ClosestRayResultCallback resultCallback(pos1, pos2); resultCallback.m_collisionFilterGroup = CollisionType_AnyPhysical; resultCallback.m_collisionFilterMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Door; MaybeLock lockColWorld(mCollisionWorldMutex, mLockingPolicy); mCollisionWorld->rayTest(pos1, pos2, resultCallback); return !resultCallback.hasHit(); } void PhysicsTaskScheduler::doSimulation() { while (mRemainingSteps) { 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]); mPostStepBarrier->wait([this] { afterPostStep(); }); } refreshLOSCache(); mPostSimBarrier->wait([this] { afterPostSim(); }); } void PhysicsTaskScheduler::updateStats(osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { if (!stats.collectStats("engine")) return; if (mFrameNumber == frameNumber - 1) { stats.setAttribute(mFrameNumber, "physicsworker_time_begin", mTimer->delta_s(mFrameStart, mTimeBegin)); stats.setAttribute(mFrameNumber, "physicsworker_time_taken", mTimer->delta_s(mTimeBegin, mTimeEnd)); stats.setAttribute(mFrameNumber, "physicsworker_time_end", mTimer->delta_s(mFrameStart, mTimeEnd)); } mFrameStart = frameStart; mTimeBegin = mTimer->tick(); mFrameNumber = frameNumber; } void PhysicsTaskScheduler::debugDraw() { 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() { updateAabbs(); if (!mRemainingSteps) return; const Visitors::PreStep impl{ mCollisionWorld }; const Visitors::WithLockedPtr vis{ impl, mCollisionWorldMutex, mLockingPolicy }; for (auto& sim : *mSimulations) std::visit(vis, sim); } void PhysicsTaskScheduler::afterPostStep() { if (mRemainingSteps) { --mRemainingSteps; updateActorsPositions(); } mNextJob.store(0, std::memory_order_release); } void PhysicsTaskScheduler::afterPostSim() { { MaybeExclusiveLock lock(mLOSCacheMutex, mLockingPolicy); mLOSCache.erase( 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(); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/mtphysics.hpp000066400000000000000000000125441503074453300234600ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_MTPHYSICS_H #define OPENMW_MWPHYSICS_MTPHYSICS_H #include #include #include #include #include #include #include #include #include #include #include "components/misc/budgetmeasurement.hpp" #include "physicssystem.hpp" #include "ptrholder.hpp" namespace Misc { class Barrier; } namespace MWRender { class DebugDrawer; } 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 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/object.cpp000066400000000000000000000143371503074453300227000ustar00rootroot00000000000000#include "object.hpp" #include "mtphysics.hpp" #include #include #include #include #include #include #include #include namespace MWPhysics { 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) { mCollisionObject = BulletHelpers::makeCollisionObject(mShapeInstance->mCollisionShape.get(), Misc::Convert::toBullet(mPosition), Misc::Convert::toBullet(rotation)); mCollisionObject->setUserPointer(this); mShapeInstance->setLocalScaling(mScale); mTaskScheduler->addCollisionObject(mCollisionObject.get(), collisionType, CollisionType_Actor | CollisionType_HeightMap | CollisionType_Projectile); } Object::~Object() { mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } const Resource::BulletShapeInstance* Object::getShapeInstance() const { return mShapeInstance.get(); } void Object::setScale(float scale) { std::unique_lock lock(mPositionMutex); mScale = { scale, scale, scale }; mScaleUpdatePending = true; } void Object::setRotation(osg::Quat quat) { std::unique_lock lock(mPositionMutex); mRotation = quat; mTransformUpdatePending = true; } void Object::updatePosition() { std::unique_lock lock(mPositionMutex); mPosition = mPtr.getRefData().getPosition().asVec3(); mTransformUpdatePending = true; } void Object::commitPositionChange() { std::unique_lock lock(mPositionMutex); if (mScaleUpdatePending) { mShapeInstance->setLocalScaling(mScale); mScaleUpdatePending = false; } if (mTransformUpdatePending) { btTransform trans; trans.setOrigin(Misc::Convert::toBullet(mPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); mCollisionObject->setWorldTransform(trans); mTransformUpdatePending = false; } } btTransform Object::getTransform() const { std::unique_lock lock(mPositionMutex); btTransform trans; trans.setOrigin(Misc::Convert::toBullet(mPosition)); trans.setRotation(Misc::Convert::toBullet(mRotation)); return trans; } bool Object::isSolid() const { return mSolid; } void Object::setSolid(bool solid) { mSolid = solid; } bool Object::isAnimated() const { return mShapeInstance->isAnimated(); } bool Object::animateCollisionShapes() { if (mShapeInstance->mAnimatedShapes.empty()) return false; if (!mPtr.getRefData().getBaseNode()) return false; assert(mShapeInstance->mCollisionShape->isCompound()); btCompoundShape* compound = static_cast(mShapeInstance->mCollisionShape.get()); bool result = false; for (const auto& [recIndex, shapeIndex] : mShapeInstance->mAnimatedShapes) { auto nodePathFound = mRecIndexToNodePath.find(recIndex); if (nodePathFound == mRecIndexToNodePath.end()) { NifOsg::FindGroupByRecIndex visitor(recIndex); mPtr.getRefData().getBaseNode()->accept(visitor); if (!visitor.mFound) { 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); return false; } osg::NodePath nodePath = visitor.mFoundPath; nodePath.erase(nodePath.begin()); nodePathFound = mRecIndexToNodePath.emplace(recIndex, nodePath).first; } 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 btCollisionShape* childShape = compound->getChildShape(shapeIndex); btVector3 newScale = compound->getLocalScaling() * scale; if (childShape->getLocalScaling() != newScale) { childShape->setLocalScaling(newScale); result = true; } if (!(transform == compound->getChildTransform(shapeIndex))) { compound->updateChildTransform(shapeIndex, transform); result = 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; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/object.hpp000066400000000000000000000037361503074453300227060ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_OBJECT_H #define OPENMW_MWPHYSICS_OBJECT_H #include "ptrholder.hpp" #include #include #include #include namespace Resource { class BulletShapeInstance; } 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, osg::Quat rotation, int collisionType, PhysicsTaskScheduler* scheduler); ~Object() override; const Resource::BulletShapeInstance* getShapeInstance() const; void setScale(float scale); void setRotation(osg::Quat quat); void updatePosition(); void commitPositionChange(); btTransform getTransform() const; /// Return solid flag. Not used by the object itself, true by default. bool isSolid() const; void setSolid(bool solid); bool isAnimated() const; /// @brief update object shape /// @return true if shape changed bool animateCollisionShapes(); bool collidedWith(ScriptedCollisionType type) const; void addCollision(ScriptedCollisionType type); void resetCollisions(); private: osg::ref_ptr mShapeInstance; std::map mRecIndexToNodePath; bool mSolid; btVector3 mScale; osg::Vec3f mPosition; osg::Quat mRotation; bool mScaleUpdatePending = false; bool mTransformUpdatePending = false; mutable std::mutex mPositionMutex; PhysicsTaskScheduler* mTaskScheduler; char mCollidedWith; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/physicssystem.cpp000066400000000000000000001075231503074453300243610ustar00rootroot00000000000000#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 "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/movement.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" #include "../mwrender/bulletdebugdraw.hpp" #include "../mwworld/class.hpp" #include "actor.hpp" #include "collisiontype.hpp" #include "closestnotmerayresultcallback.hpp" #include "contacttestresultcallback.hpp" #include "hasspherecollisioncallback.hpp" #include "heightfield.hpp" #include "movementsolver.hpp" #include "mtphysics.hpp" #include "object.hpp" #include "projectile.hpp" namespace { void handleJump(const MWWorld::Ptr& ptr) { if (!ptr.getClass().isActor()) return; if (ptr.getClass().getMovementSettings(ptr).mPosition[2] == 0) return; const bool isPlayer = (ptr == MWMechanics::getPlayer()); // Advance acrobatics and set flag for GetPCJumping if (isPlayer) { ptr.getClass().skillUsageSucceeded(ptr, ESM::Skill::Acrobatics, 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( 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(std::move(parentNode)) , mPhysicsDt(1.f / 60.f) { mResourceSystem->addResourceManager(mShapeManager.get()); mCollisionConfiguration = std::make_unique(); mDispatcher = std::make_unique(mCollisionConfiguration.get()); mBroadphase = std::make_unique(); 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. mCollisionWorld->setForceUpdateAllAabbs(false); // Check if a user decided to override a physics system FPS if (const char* env = getenv("OPENMW_PHYSICS_FPS")) { 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)."; } } mDebugDrawer = std::make_unique(mParentNode, mCollisionWorld.get(), mDebugDrawEnabled); mTaskScheduler = std::make_unique(mPhysicsDt, mCollisionWorld.get(), mDebugDrawer.get()); } PhysicsSystem::~PhysicsSystem() { mResourceSystem->removeResourceManager(mShapeManager.get()); if (mWaterCollisionObject) mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); mTaskScheduler->releaseSharedStates(); mHeightFields.clear(); mObjects.clear(); mActors.clear(); mProjectiles.clear(); } Resource::BulletShapeManager* PhysicsSystem::getShapeManager() { return mShapeManager.get(); } bool PhysicsSystem::toggleDebugRendering() { mDebugDrawEnabled = !mDebugDrawEnabled; mCollisionWorld->setDebugDrawer(mDebugDrawEnabled ? mDebugDrawer.get() : nullptr); mDebugDrawer->setDebugMode(mDebugDrawEnabled); return mDebugDrawEnabled; } void PhysicsSystem::markAsNonSolid(const MWWorld::ConstPtr& 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 { const Actor* physactor = getActor(actor); 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.mRef); if (foundObj == mObjects.end()) return false; if (!foundObj->second->isSolid()) return false; return true; } 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) { RayCastingResult result; result.mHit = false; return result; } btVector3 btFrom = Misc::Convert::toBullet(from); btVector3 btTo = Misc::Convert::toBullet(to); std::vector ignoreList; std::vector targetCollisionObjects; for (const auto& ptr : ignore) { if (!ptr.isEmpty()) { 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 (const MWWorld::Ptr& target : targets) { const Actor* actor = getActor(target); if (actor) targetCollisionObjects.push_back(actor->getCollisionObject()); } } ClosestNotMeRayResultCallback resultCallback(ignoreList, targetCollisionObjects, btFrom, btTo); resultCallback.m_collisionFilterGroup = group; resultCallback.m_collisionFilterMask = mask; mTaskScheduler->rayTest(btFrom, btTo, resultCallback); RayCastingResult result; result.mHit = resultCallback.hasHit(); if (resultCallback.hasHit()) { result.mHitPos = Misc::Convert::toOsg(resultCallback.m_hitPointWorld); result.mHitNormal = Misc::Convert::toOsg(resultCallback.m_hitNormalWorld); if (PtrHolder* ptrHolder = static_cast(resultCallback.m_collisionObject->getUserPointer())) result.mHitObject = ptrHolder->getPtr(); } return result; } 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 = 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)); mTaskScheduler->convexSweepTest(&shape, from_, to_, callback); RayCastingResult result; result.mHit = callback.hasHit(); if (result.mHit) { 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 { if (actor1 == actor2) return true; const auto it1 = mActors.find(actor1.mRef); const auto it2 = mActors.find(actor2.mRef); if (it1 == mActors.end() || it2 == mActors.end()) return false; return mTaskScheduler->getLineOfSight(it1->second, it2->second); } bool PhysicsSystem::isOnGround(const MWWorld::Ptr& actor) { Actor* physactor = getActor(actor); return physactor && physactor->getOnGround() && physactor->getCollisionMode(); } bool PhysicsSystem::canMoveToWaterSurface(const MWWorld::ConstPtr& actor, const float waterlevel) { const auto* physactor = getActor(actor); return physactor && physactor->canMoveToWaterSurface(waterlevel, mCollisionWorld.get()); } osg::Vec3f PhysicsSystem::getHalfExtents(const MWWorld::ConstPtr& actor) const { const Actor* physactor = getActor(actor); if (physactor) return physactor->getHalfExtents(); else return osg::Vec3f(); } osg::Vec3f PhysicsSystem::getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const { if (const Actor* physactor = getActor(actor)) return physactor->getOriginalHalfExtents(); else return osg::Vec3f(); } osg::Vec3f PhysicsSystem::getRenderingHalfExtents(const MWWorld::ConstPtr& actor) const { const Actor* physactor = getActor(actor); if (physactor) return physactor->getRenderingHalfExtents(); else return osg::Vec3f(); } osg::BoundingBox PhysicsSystem::getBoundingBox(const MWWorld::ConstPtr& object) const { 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 { const Actor* physactor = getActor(actor); if (physactor) return physactor->getCollisionObjectPosition(); else return osg::Vec3f(); } std::vector PhysicsSystem::getCollisionsPoints( const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const { btCollisionObject* me = nullptr; auto found = mObjects.find(ptr.mRef); if (found != mObjects.end()) me = found->second->getCollisionObject(); else return {}; 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 actors; for (auto& [actor, point, normal] : getCollisionsPoints(ptr, collisionGroup, collisionMask)) actors.emplace_back(actor); return actors; } osg::Vec3f PhysicsSystem::traceDown(const MWWorld::Ptr& ptr, const osg::Vec3f& position, float maxHeight) { 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, int size, int verts, float minH, float maxH, const osg::Object* holdObject) { 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) { HeightFieldMap::iterator heightfield = mHeightFields.find(std::make_pair(x, y)); if (heightfield != mHeightFields.end()) mHeightFields.erase(heightfield); } const HeightField* PhysicsSystem::getHeightField(int x, int y) const { const auto heightField = mHeightFields.find(std::make_pair(x, y)); if (heightField == mHeightFields.end()) return nullptr; return heightField->second.get(); } void PhysicsSystem::addObject( const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh, osg::Quat rotation, int collisionType) { if (ptr.mRef->mData.mPhysicsPostponed) return; const VFS::Path::Normalized animationMesh = ptr.getClass().useAnim() ? Misc::ResourceHelpers::correctActorModelPath(mesh, mResourceSystem->getVFS()) : VFS::Path::Normalized(mesh); 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.emplace(obj.get(), false); } void PhysicsSystem::remove(const MWWorld::Ptr& ptr) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { mAnimatedObjects.erase(foundObject->second.get()); mObjects.erase(foundObject); } else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { mActors.erase(foundActor); } } void PhysicsSystem::removeProjectile(const int projectileId) { ProjectileMap::iterator foundProjectile = mProjectiles.find(projectileId); if (foundProjectile != mProjectiles.end()) mProjectiles.erase(foundProjectile); } void PhysicsSystem::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated) { if (auto foundObject = mObjects.find(old.mRef); foundObject != mObjects.end()) foundObject->second->updatePtr(updated); else if (auto foundActor = mActors.find(old.mRef); foundActor != mActors.end()) foundActor->second->updatePtr(updated); for (auto& [_, actor] : mActors) { if (actor->getStandingOnPtr() == old) actor->setStandingOnPtr(updated); } for (auto& [_, projectile] : mProjectiles) { if (projectile->getCaster() == old) projectile->setCaster(updated); } } Actor* PhysicsSystem::getActor(const MWWorld::Ptr& 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 { 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 { ObjectMap::const_iterator found = mObjects.find(ptr.mRef); if (found != mObjects.end()) return found->second.get(); return nullptr; } Projectile* PhysicsSystem::getProjectile(int projectileId) const { ProjectileMap::const_iterator found = mProjectiles.find(projectileId); if (found != mProjectiles.end()) return found->second.get(); return nullptr; } void PhysicsSystem::updateScale(const MWWorld::Ptr& ptr) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { float scale = ptr.getCellRef().getScale(); foundObject->second->setScale(scale); mTaskScheduler->updateSingleAabb(foundObject->second); } else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updateScale(); mTaskScheduler->updateSingleAabb(foundActor->second); } } void PhysicsSystem::updateRotation(const MWWorld::Ptr& ptr, osg::Quat rotate) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { foundObject->second->setRotation(rotate); mTaskScheduler->updateSingleAabb(foundObject->second); } else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { if (!foundActor->second->isRotationallyInvariant()) { foundActor->second->setRotation(rotate); mTaskScheduler->updateSingleAabb(foundActor->second); } } } void PhysicsSystem::updatePosition(const MWWorld::Ptr& ptr) { if (auto foundObject = mObjects.find(ptr.mRef); foundObject != mObjects.end()) { foundObject->second->updatePosition(); mTaskScheduler->updateSingleAabb(foundObject->second); } else if (auto foundActor = mActors.find(ptr.mRef); foundActor != mActors.end()) { foundActor->second->updatePosition(); mTaskScheduler->updateSingleAabb(foundActor->second, true); } } void PhysicsSystem::addActor(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh) { const VFS::Path::Normalized 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.mExtents.length2() == 0) { if (animationMesh != mesh) { shape = mShapeManager->getShape(mesh); } } if (!shape) return; // 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, VFS::Path::NormalizedView mesh, bool computeRadius) { osg::ref_ptr shapeInstance = mShapeManager->getInstance(mesh); assert(shapeInstance); float radius = computeRadius ? shapeInstance->mCollisionBox.mExtents.length() / 2.f : 1.f; mProjectileId++; auto projectile = std::make_shared(caster, position, radius, mTaskScheduler.get(), this); mProjectiles.emplace(mProjectileId, std::move(projectile)); return mProjectileId; } void PhysicsSystem::setCaster(int projectileId, const MWWorld::Ptr& caster) { const auto foundProjectile = mProjectiles.find(projectileId); assert(foundProjectile != mProjectiles.end()); auto* projectile = foundProjectile->second.get(); projectile->setCaster(caster); } bool PhysicsSystem::toggleCollisionMode() { ActorMap::iterator found = mActors.find(MWMechanics::getPlayer().mRef); if (found != mActors.end()) { bool cmode = found->second->getCollisionMode(); cmode = !cmode; found->second->enableCollisionMode(cmode); // NB: Collision body isn't disabled for vanilla TCL compatibility return cmode; } return false; } void PhysicsSystem::queueObjectMovement(const MWWorld::Ptr& ptr, const osg::Vec3f& velocity) { ActorMap::iterator found = mActors.find(ptr.mRef); if (found != mActors.end()) found->second->setVelocity(velocity); } void PhysicsSystem::clearQueuedMovement() { for (const auto& [_, actor] : mActors) { actor->setVelocity(osg::Vec3f()); actor->setInertialForce(osg::Vec3f()); } } void PhysicsSystem::prepareSimulation(bool willSimulate, std::vector& simulations) { 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()) waterlevel = cell->getWaterLevel(); const auto& stats = ptr.getClass().getCreatureStats(ptr); const MWMechanics::MagicEffects& effects = stats.getMagicEffects(); bool waterCollision = false; if (cell->getCell()->hasWater() && effects.getOrDefault(ESM::MagicEffect::WaterWalking).getMagnitude()) { 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::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 } }); // if the simulation will run, a jump request will be fulfilled. Update mechanics accordingly. if (willSimulate) handleJump(ptr); } for (const auto& [id, projectile] : mProjectiles) { simulations.emplace_back(ProjectileSimulation{ projectile, ProjectileFrameData{ *projectile } }); } } void PhysicsSystem::stepSimulation( float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats) { for (auto& [animatedObject, changed] : mAnimatedObjects) { if (animatedObject->animateCollisionShapes()) { 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.mRef); if (found != mObjects.end()) if (found->second->animateCollisionShapes()) mTaskScheduler->updateSingleAabb(found->second); } void PhysicsSystem::debugDraw() { if (mDebugDrawEnabled) mTaskScheduler->debugDraw(); } bool PhysicsSystem::isActorStandingOn(const MWWorld::Ptr& actor, const MWWorld::ConstPtr& object) const { 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 { for (const auto& [_, actor] : mActors) { if (actor->getStandingOnPtr() == object) out.emplace_back(actor->getPtr()); } } bool PhysicsSystem::isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const { 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 { std::vector collisions = getCollisions(object, CollisionType_World, CollisionType_Actor); out.insert(out.end(), collisions.begin(), collisions.end()); } void PhysicsSystem::disableWater() { if (mWaterEnabled) { mWaterEnabled = false; updateWater(); } } void PhysicsSystem::enableWater(float height) { if (!mWaterEnabled || mWaterHeight != height) { mWaterEnabled = true; mWaterHeight = height; updateWater(); } } void PhysicsSystem::setWaterHeight(float height) { if (mWaterHeight != height) { mWaterHeight = height; updateWater(); } } void PhysicsSystem::updateWater() { if (mWaterCollisionObject) { mTaskScheduler->removeCollisionObject(mWaterCollisionObject.get()); } if (!mWaterEnabled) { mWaterCollisionObject.reset(); return; } 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, 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 = 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(); } void PhysicsSystem::reportStats(unsigned int frameNumber, osg::Stats& stats) const { 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()); } void PhysicsSystem::reportCollision(const btVector3& position, const btVector3& normal) { if (mDebugDrawEnabled) mDebugDrawer->addCollision(position, normal); } 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) { } 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) { // 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) auto* raw1 = a1.lock().get(); auto* raw2 = a2.lock().get(); assert(raw1 != raw2); if (raw1 < raw2) { mActors = { a1, a2 }; mRawActors = { raw1, raw2 }; } else { mActors = { a2, a1 }; mRawActors = { raw2, raw1 }; } } bool operator==(const LOSRequest& lhs, const LOSRequest& rhs) noexcept { return lhs.mRawActors == rhs.mRawActors; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/physicssystem.hpp000066400000000000000000000277021503074453300243660ustar00rootroot00000000000000#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 "../mwworld/ptr.hpp" #include "collisiontype.hpp" #include "raycasting.hpp" namespace osg { class Group; class Object; class Stats; } namespace MWRender { class DebugDrawer; } namespace Resource { class BulletShapeManager; class ResourceSystem; } class btCollisionWorld; class btBroadphaseInterface; class btDefaultCollisionConfiguration; class btCollisionDispatcher; class btCollisionObject; class btCollisionShape; class btVector3; namespace MWPhysics { class HeightField; class Object; class Actor; class PhysicsTaskScheduler; class Projectile; enum ScriptedCollisionType : char; using ActorMap = std::unordered_map>; struct ContactPoint { MWWorld::Ptr mObject; osg::Vec3f mPoint; osg::Vec3f mNormal; }; struct LOSRequest { LOSRequest(const std::weak_ptr& a1, const std::weak_ptr& a2); std::array, 2> mActors; std::array mRawActors; bool mResult; bool mStale; int mAge; }; bool operator==(const LOSRequest& lhs, const LOSRequest& rhs) noexcept; struct ActorFrameData { 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; osg::Vec3f mMovement; const btCollisionObject* mCaster; const btCollisionObject* mCollisionObject; Projectile* mProjectile; }; struct WorldFrameData { WorldFrameData(); bool mIsInStorm; osg::Vec3f mStormDirection; }; template class SimulationImpl { 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; }; using ActorSimulation = SimulationImpl; using ProjectileSimulation = SimulationImpl; using Simulation = std::variant; class PhysicsSystem : public RayCastingInterface { public: PhysicsSystem(Resource::ResourceSystem* resourceSystem, osg::ref_ptr parentNode); virtual ~PhysicsSystem(); Resource::BulletShapeManager* getShapeManager(); void enableWater(float height); void setWaterHeight(float height); void disableWater(); void addObject(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh, osg::Quat rotation, int collisionType = CollisionType_World); void addActor(const MWWorld::Ptr& ptr, VFS::Path::NormalizedView mesh); int addProjectile( const MWWorld::Ptr& caster, const osg::Vec3f& position, VFS::Path::NormalizedView mesh, bool computeRadius); void setCaster(int projectileId, const MWWorld::Ptr& caster); void removeProjectile(const int projectileId); void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated); Actor* getActor(const MWWorld::Ptr& ptr); const Actor* getActor(const MWWorld::ConstPtr& ptr) const; const Object* getObject(const MWWorld::ConstPtr& ptr) const; Projectile* getProjectile(int projectileId) const; // Object or Actor void remove(const MWWorld::Ptr& ptr); void updateScale(const MWWorld::Ptr& ptr); void updateRotation(const MWWorld::Ptr& ptr, osg::Quat rotate); void updatePosition(const MWWorld::Ptr& ptr); void addHeightField(const float* heights, int x, int y, int size, int verts, float minH, float maxH, const osg::Object* holdObject); void removeHeightField(int x, int y); const HeightField* getHeightField(int x, int y) const; bool toggleCollisionMode(); /// Determine new position based on all queued movements, then clear the list. void stepSimulation( float dt, bool skipSimulation, osg::Timer_t frameStart, unsigned int frameNumber, osg::Stats& stats); /// Apply new positions to actors void moveActors(); void debugDraw(); std::vector getCollisions(const MWWorld::ConstPtr& ptr, int collisionGroup, int collisionMask) const; ///< get handles this object collides with 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); /// @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; RayCastingResult castSphere(const osg::Vec3f& from, const osg::Vec3f& to, float radius, int mask = CollisionType_Default, int group = 0xff) const override; /// Return true if actor1 can see actor2. bool getLineOfSight(const MWWorld::ConstPtr& actor1, const MWWorld::ConstPtr& actor2) const override; bool isOnGround(const MWWorld::Ptr& actor); bool canMoveToWaterSurface(const MWWorld::ConstPtr& actor, const float waterlevel); /// Get physical half extents (scaled) of the given actor. osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& actor) const; /// Get physical half extents (not scaled) of the given actor. osg::Vec3f getOriginalHalfExtents(const MWWorld::ConstPtr& actor) const; /// @see MWPhysics::Actor::getRenderingHalfExtents osg::Vec3f getRenderingHalfExtents(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 bounding box in world space of the given object. osg::BoundingBox getBoundingBox(const MWWorld::ConstPtr& object) const; /// Queues velocity movement for a Ptr. If a Ptr is already queued, its velocity will /// be overwritten. Valid until the next call to stepSimulation void queueObjectMovement(const MWWorld::Ptr& ptr, const osg::Vec3f& velocity); /// Clear the queued movements list without applying. void clearQueuedMovement(); /// 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; /// 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 an object of the given type has collided with this object bool isObjectCollidingWith(const MWWorld::ConstPtr& object, ScriptedCollisionType type) const; /// Get the handle of all actors colliding with \a object in this frame. void getActorsCollidingWith(const MWWorld::ConstPtr& object, std::vector& out) const; bool toggleDebugRendering(); /// 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 isOnSolidGround(const MWWorld::Ptr& actor) const; void updateAnimatedCollisionShape(const MWWorld::Ptr& object); template void forEachAnimatedObject(Function&& function) const { std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); } bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, std::span ignore, std::vector* occupyingActors) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportCollision(const btVector3& position, const btVector3& normal); private: void updateWater(); void prepareSimulation(bool willSimulate, std::vector& simulations); 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; using ObjectMap = std::unordered_map>; ObjectMap mObjects; std::map mAnimatedObjects; // stores pointers to elements in mObjects ActorMap mActors; using ProjectileMap = std::map>; ProjectileMap mProjectiles; using HeightFieldMap = std::map, std::unique_ptr>; HeightFieldMap mHeightFields; bool mDebugDrawEnabled; float mTimeAccum; unsigned int mProjectileId; float mWaterHeight; bool mWaterEnabled; std::unique_ptr mWaterCollisionObject; std::unique_ptr mWaterCollisionShape; std::unique_ptr mDebugDrawer; osg::ref_ptr mParentNode; float mPhysicsDt; std::size_t mSimulationsCounter = 0; std::array, 2> mSimulations; std::vector> mActorsPositions; PhysicsSystem(const PhysicsSystem&); PhysicsSystem& operator=(const PhysicsSystem&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/projectile.cpp000066400000000000000000000076401503074453300235710ustar00rootroot00000000000000#include #include #include #include "actor.hpp" #include "collisiontype.hpp" #include "mtphysics.hpp" #include "object.hpp" #include "projectile.hpp" namespace MWPhysics { 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()); mCollisionObject = std::make_unique(); mCollisionObject->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); mCollisionObject->setActivationState(DISABLE_DEACTIVATION); mCollisionObject->setCollisionShape(mShape.get()); mCollisionObject->setUserPointer(this); mPosition = position; mPreviousPosition = position; mSimulationPosition = position; setCaster(caster); const int collisionMask = CollisionType_World | CollisionType_HeightMap | CollisionType_Actor | CollisionType_Door | CollisionType_Water | CollisionType_Projectile; mTaskScheduler->addCollisionObject(mCollisionObject.get(), CollisionType_Projectile, collisionMask); updateCollisionObjectPosition(); } Projectile::~Projectile() { if (!mActive) mPhysics->reportCollision(mHitPosition, mHitNormal); mTaskScheduler->removeCollisionObject(mCollisionObject.get()); } void Projectile::updateCollisionObjectPosition() { std::scoped_lock lock(mMutex); auto& trans = mCollisionObject->getWorldTransform(); trans.setOrigin(Misc::Convert::toBullet(mPosition)); mCollisionObject->setWorldTransform(trans); } 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; } MWWorld::Ptr Projectile::getTarget() const { assert(!mActive); auto* target = static_cast(mHitTarget->getUserPointer()); return target ? target->getPtr() : MWWorld::Ptr(); } MWWorld::Ptr Projectile::getCaster() const { return mCaster; } 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; }(); } void Projectile::setValidTargets(const std::vector& targets) { std::scoped_lock lock(mMutex); mValidTargets.clear(); for (const auto& ptr : targets) { const auto* physicActor = mPhysics->getActor(ptr); if (physicActor) mValidTargets.push_back(physicActor->getCollisionObject()); } } bool Projectile::isValidTarget(const btCollisionObject* target) const { assert(target); std::scoped_lock lock(mMutex); if (mCasterColObj == target) return false; if (mValidTargets.empty()) return true; return std::any_of(mValidTargets.begin(), mValidTargets.end(), [target](const btCollisionObject* actor) { return target == actor; }); } } openmw-openmw-0.49.0/apps/openmw/mwphysics/projectile.hpp000066400000000000000000000040621503074453300235710ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_PROJECTILE_H #define OPENMW_MWPHYSICS_PROJECTILE_H #include #include #include #include #include "ptrholder.hpp" class btCollisionObject; class btCollisionShape; class btConvexShape; namespace osg { class Vec3f; } namespace MWPhysics { class PhysicsTaskScheduler; class PhysicsSystem; class Projectile final : public PtrHolder { public: Projectile(const MWWorld::Ptr& caster, const osg::Vec3f& position, float radius, PhysicsTaskScheduler* scheduler, PhysicsSystem* physicssystem); ~Projectile() override; btConvexShape* getConvexShape() const { return mConvexShape; } void updateCollisionObjectPosition(); bool isActive() const { return mActive.load(std::memory_order_acquire); } MWWorld::Ptr getTarget() const; MWWorld::Ptr getCaster() const; void setCaster(const MWWorld::Ptr& caster); const btCollisionObject* getCasterCollisionObject() const { return mCasterColObj; } void setHitWater() { mHitWater = true; } bool getHitWater() const { return mHitWater; } void hit(const btCollisionObject* target, btVector3 pos, btVector3 normal); void setValidTargets(const std::vector& targets); bool isValidTarget(const btCollisionObject* target) const; btVector3 getHitPosition() const { return mHitPosition; } private: std::unique_ptr mShape; btConvexShape* mConvexShape; bool mHitWater; std::atomic mActive; MWWorld::Ptr mCaster; const btCollisionObject* mCasterColObj; const btCollisionObject* mHitTarget; btVector3 mHitPosition; btVector3 mHitNormal; std::vector mValidTargets; mutable std::mutex mMutex; PhysicsSystem* mPhysics; PhysicsTaskScheduler* mTaskScheduler; Projectile(const Projectile&); Projectile& operator=(const Projectile&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/projectileconvexcallback.cpp000066400000000000000000000030241503074453300264610ustar00rootroot00000000000000#include #include "collisiontype.hpp" #include "projectile.hpp" #include "projectileconvexcallback.hpp" namespace MWPhysics { btScalar ProjectileConvexCallback::addSingleResult( btCollisionWorld::LocalConvexResult& result, bool normalInWorldSpace) { const auto* hitObject = result.m_hitCollisionObject; // don't hit the caster if (hitObject == mCaster) return 1.f; // don't hit the projectile if (hitObject == mMe) return 1.f; btCollisionWorld::ClosestConvexResultCallback::addSingleResult(result, normalInWorldSpace); switch (hitObject->getBroadphaseHandle()->m_collisionFilterGroup) { case CollisionType_Actor: { if (!mProjectile.isValidTarget(hitObject)) return 1.f; break; } case CollisionType_Projectile: { 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.setHitWater(); break; } } mProjectile.hit(hitObject, m_hitPointWorld, m_hitNormalWorld); return result.m_hitFraction; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/projectileconvexcallback.hpp000066400000000000000000000017131503074453300264710ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H #define OPENMW_MWPHYSICS_PROJECTILECONVEXCALLBACK_H #include class btCollisionObject; namespace MWPhysics { class Projectile; class ProjectileConvexCallback : public btCollisionWorld::ClosestConvexResultCallback { public: 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/ptrholder.hpp000066400000000000000000000032421503074453300234330ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_PTRHOLDER_H #define OPENMW_MWPHYSICS_PTRHOLDER_H #include #include #include #include #include #include "../mwworld/ptr.hpp" namespace MWPhysics { class PtrHolder { public: explicit PtrHolder(const MWWorld::Ptr& ptr, const osg::Vec3f& position) : mPtr(ptr) , mSimulationPosition(position) , mPosition(position) , mPreviousPosition(position) { } virtual ~PtrHolder() = default; void updatePtr(const MWWorld::Ptr& updated) { mPtr = updated; } MWWorld::Ptr getPtr() const { return mPtr; } btCollisionObject* getCollisionObject() const { return mCollisionObject.get(); } void setVelocity(osg::Vec3f velocity) { mVelocity = velocity; } osg::Vec3f velocity() { return std::exchange(mVelocity, osg::Vec3f()); } void setSimulationPosition(const osg::Vec3f& position) { mSimulationPosition = position; } osg::Vec3f getSimulationPosition() const { return mSimulationPosition; } void setPosition(const osg::Vec3f& position) { mPreviousPosition = mPosition; mPosition = position; } osg::Vec3d getPosition() const { return mPosition; } osg::Vec3d getPreviousPosition() const { return mPreviousPosition; } protected: MWWorld::Ptr mPtr; std::unique_ptr mCollisionObject; osg::Vec3f mVelocity; osg::Vec3f mSimulationPosition; osg::Vec3d mPosition; osg::Vec3d mPreviousPosition; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/raycasting.hpp000066400000000000000000000025731503074453300236020ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_RAYCASTING_H #define OPENMW_MWPHYSICS_RAYCASTING_H #include #include "../mwworld/ptr.hpp" #include "collisiontype.hpp" namespace MWPhysics { class RayCastingResult { public: bool mHit; osg::Vec3f mHitPos; osg::Vec3f mHitNormal; MWWorld::Ptr mHitObject; }; class RayCastingInterface { 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/stepper.cpp000066400000000000000000000166451503074453300231200ustar00rootroot00000000000000#include "stepper.hpp" #include #include #include #include "collisiontype.hpp" #include "constants.hpp" #include "movementsolver.hpp" namespace MWPhysics { static bool canStepDown(const ActorTracer& stepper) { if (!stepper.mHitObject) return false; 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) : mColWorld(colWorld) , mColObj(colObj) { } 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) 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. mUpStepper.doTrace( mColObj, position, position + osg::Vec3f(0.0f, 0.0f, Constants::sStepSizeUp), mColWorld, onGround); float upDistance = 0; if (!mUpStepper.mHitObject) upDistance = Constants::sStepSizeUp; else if (mUpStepper.mFraction * Constants::sStepSizeUp > sCollisionMargin) upDistance = mUpStepper.mFraction * Constants::sStepSizeUp - sCollisionMargin; else { return false; } auto toMove = velocity * remainingTime; osg::Vec3f tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); osg::Vec3f tracerDest; 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. int attempt = 0; float downStepSize = 0; while (attempt < 3) { attempt++; if (attempt == 1) tracerDest = tracerPos + toMove; else if (!sDoExtraStairHacks) // early out if we have extra hacks disabled { return false; } else if (attempt == 2) { moveDistance = sMinStep; tracerDest = tracerPos + normalMove * sMinStep; } else if (attempt == 3) { if (upDistance > Constants::sStepSizeUp) { upDistance = Constants::sStepSizeUp; tracerPos = position + osg::Vec3f(0.0f, 0.0f, upDistance); } moveDistance = sMinStep2; tracerDest = tracerPos + normalMove * sMinStep2; } mTracer.doTrace(mColObj, tracerPos, tracerDest, mColWorld); 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 { return false; } moveDistance -= sCollisionMargin; tracerDest = tracerPos + normalMove * moveDistance; // safely eject from what we hit by the safety margin 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) { 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 downStepSize = upDistance; else downStepSize = moveDistance + upDistance + sStepSizeDown; 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)) { break; } else { // 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)) { return false; } // do next attempt if first iteration of movement solver and not out of attempts if (firstIteration && attempt < 3) { continue; } return false; } } // 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 (downDistance - sCollisionMargin - sGroundOffset > upDistance && !onGround) return false; auto newpos = tracerDest + osg::Vec3f(0.0f, 0.0f, -downDistance); if ((position - newpos).length2() < sCollisionMargin * sCollisionMargin) return false; if (mTracer.mHitObject) { auto planeNormal = mTracer.mPlaneNormal; if (onGround && !isWalkableSlope(planeNormal) && planeNormal.z() != 0) { planeNormal.z() = 0; planeNormal.normalize(); } velocity = reject(velocity, planeNormal); } velocity = reject(velocity, mDownStepper.mPlaneNormal); position = newpos; remainingTime *= (1.0f - mTracer.mFraction); // remaining time is proportional to remaining distance return true; } } openmw-openmw-0.49.0/apps/openmw/mwphysics/stepper.hpp000066400000000000000000000011701503074453300231100ustar00rootroot00000000000000#ifndef OPENMW_MWPHYSICS_STEPPER_H #define OPENMW_MWPHYSICS_STEPPER_H #include "trace.h" class btCollisionObject; class btCollisionWorld; namespace osg { class Vec3f; } namespace MWPhysics { class Stepper { private: const btCollisionWorld* mColWorld; const btCollisionObject* mColObj; ActorTracer mTracer, mUpStepper, mDownStepper; public: Stepper(const btCollisionWorld* colWorld, const btCollisionObject* colObj); bool step(osg::Vec3f& position, osg::Vec3f& velocity, float& remainingTime, const bool& onGround, bool firstIteration); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwphysics/trace.cpp000066400000000000000000000132271503074453300225250ustar00rootroot00000000000000#include "trace.h" #include #include #include #include "actor.hpp" #include "actorconvexcallback.hpp" #include "collisiontype.hpp" namespace MWPhysics { 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 btCollisionShape* shape = actor->getCollisionShape(); assert(shape->isConvex()); const btVector3 motion = from - to; // FIXME: this is backwards; means ActorConvexCallback is doing dot product tests backwards too ActorConvexCallback traceCallback(actor, motion, btScalar(0.0), world); // Inherit the actor's collision group and mask traceCallback.m_collisionFilterGroup = actor->getBroadphaseHandle()->m_collisionFilterGroup; traceCallback.m_collisionFilterMask = actor->getBroadphaseHandle()->m_collisionFilterMask; if (actorFilter) traceCallback.m_collisionFilterMask &= ~CollisionType_Actor; world->convexSweepTest(static_cast(shape), transFrom, transTo, traceCallback); return traceCallback; } void ActorTracer::doTrace(const btCollisionObject* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world, bool attempt_short_trace) { const btVector3 btstart = Misc::Convert::toBullet(start); btVector3 btend = Misc::Convert::toBullet(end); // Because Bullet's collision trace tests touch *all* geometry in its path, a lot of long collision tests // will unnecessarily test against complex meshes that are dozens of units away. This wouldn't normally be // a problem, but bullet isn't the fastest in the world when it comes to doing tests against triangle meshes. // Therefore, we try out a short trace first, then only fall back to the full length trace if needed. // This trace needs to be at least a couple units long, but there's no one particular ideal length. // The length of 2.1 chosen here is a "works well in practice after testing a few random lengths" value. // (Also, we only do this short test if the intended collision trace is long enough for it to make sense.) const float fallback_length = 2.1f; bool doing_short_trace = false; // For some reason, typical scenes perform a little better if we increase the threshold length for the length // test. (Multiplying by 2 in 'square distance' units gives us about 1.4x the threshold length. In benchmarks // this was // slightly better for the performance of normal scenes than 4.0, and just plain better than 1.0.) if (attempt_short_trace && (btend - btstart).length2() > fallback_length * fallback_length * 2.0) { btend = btstart + (btend - btstart).normalized() * fallback_length; doing_short_trace = true; } const auto traceCallback = sweepHelper(actor, btstart, btend, world, false); // Copy the hit data over to our trace results struct: if (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); 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; } } void ActorTracer::findGround( const Actor* actor, const osg::Vec3f& start, const osg::Vec3f& end, const btCollisionWorld* world) { 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; } } } openmw-openmw-0.49.0/apps/openmw/mwphysics/trace.h000066400000000000000000000012741503074453300221710ustar00rootroot00000000000000#ifndef OENGINE_BULLET_TRACE_H #define OENGINE_BULLET_TRACE_H #include class btCollisionObject; class btCollisionWorld; namespace MWPhysics { class Actor; struct ActorTracer { osg::Vec3f mEndPos; osg::Vec3f mPlaneNormal; osg::Vec3f mHitPoint; const btCollisionObject* mHitObject; float mFraction; 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/000077500000000000000000000000001503074453300205135ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwrender/.gitignore000066400000000000000000000000041503074453300224750ustar00rootroot00000000000000old openmw-openmw-0.49.0/apps/openmw/mwrender/actoranimation.cpp000066400000000000000000000562621503074453300242420ustar00rootroot00000000000000#include "actoranimation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.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, std::move(parentNode), resourceSystem) { MWWorld::ContainerStore& store = mPtr.getClass().getContainerStore(mPtr); for (MWWorld::ConstContainerStoreIterator iter = store.cbegin(MWWorld::ContainerStore::Type_Light); iter != store.cend(); ++iter) { 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(); } ActorAnimation::~ActorAnimation() { removeFromSceneImpl(); } PartHolderPtr ActorAnimation::attachMesh( VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor) { osg::Group* parent = getBoneByName(bonename); if (!parent) return nullptr; osg::ref_ptr instance = mResourceSystem->getSceneManager()->getInstance(model, parent); const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) return {}; if (glowColor != nullptr) mGlowUpdater = SceneUtil::addEnchantedGlow(instance, mResourceSystem, *glowColor); return std::make_unique(instance); } osg::ref_ptr ActorAnimation::attach( VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight) { osg::ref_ptr templateNode = mResourceSystem->getSceneManager()->getTemplate(model); const NodeMap& nodeMap = getNodeMap(); auto found = nodeMap.find(bonename); if (found == nodeMap.end()) throw std::runtime_error("Can't find attachment node " + 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()); } 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()) { const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); const MWWorld::Store& partStore = store.get(); for (const auto& part : bodyparts) { 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(VFS::Path::Normalized(bodypart->mModel)).value(); } } } return shield.getClass().getCorrectedModel(shield); } std::string ActorAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { std::string mesh = getShieldMesh(shield, false); if (mesh.empty()) 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(); } return mesh; } bool ActorAnimation::updateCarriedLeftVisible(const int weaptype) const { if (Settings::game().mShieldSheathing && mObjectRoot) { const MWWorld::Class& cls = mPtr.getClass(); MWMechanics::CreatureStats& stats = cls.getCreatureStats(mPtr); if (cls.hasInventoryStore(mPtr) && stats.getDrawState() == MWMechanics::DrawState::Nothing) { SceneUtil::FindByNameVisitor findVisitor("Bip01 AttachShield"); mObjectRoot->accept(findVisitor); if (findVisitor.mFoundNode) { 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); } void ActorAnimation::updateHolsteredShield(bool showCarriedLeft) { if (!Settings::game().mShieldSheathing) return; if (!mPtr.getClass().hasInventoryStore(mPtr)) return; mHolsteredShield.reset(); 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; // 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 VFS::Path::Normalized mesh = getSheathedShieldMesh(*shield); if (mesh.empty()) return; constexpr std::string_view boneName = "Bip01 AttachShield"; const bool isEnchanted = !shield->getClass().getEnchantment(*shield).empty(); const osg::Vec4f glowColor = isEnchanted ? shield->getClass().getEnchantmentColor(*shield) : osg::Vec4f(); const VFS::Path::Normalized holsteredName = addSuffixBeforeExtension(mesh, "_sh"); // If we have no dedicated sheath model, use basic shield model as fallback. if (!mResourceSystem->getVFS()->exists(holsteredName)) mHolsteredShield = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); else mHolsteredShield = attachMesh(holsteredName, boneName, isEnchanted ? &glowColor : nullptr); if (!mHolsteredShield) return; 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(mesh, shieldNode); if (isEnchanted) SceneUtil::addEnchantedGlow(shieldNode, mResourceSystem, glowColor); } } bool ActorAnimation::useShieldAnimations() const { if (!Settings::game().mShieldSheathing) return false; const MWWorld::Class& cls = mPtr.getClass(); if (!cls.hasInventoryStore(mPtr)) return false; 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->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; } return false; } osg::Group* ActorAnimation::getBoneByName(std::string_view boneName) const { if (!mObjectRoot) return nullptr; SceneUtil::FindByNameVisitor findVisitor(boneName); mObjectRoot->accept(findVisitor); return findVisitor.mFoundNode; } std::string_view ActorAnimation::getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon) { if (weapon.isEmpty()) return {}; 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; } return {}; } void ActorAnimation::resetControllers(osg::Node* node) { 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); } void ActorAnimation::updateHolsteredWeapon(bool showHolsteredWeapons) { if (!Settings::game().mWeaponSheathing) 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->getType() != ESM::Weapon::sRecordId) return; // Since throwing weapons stack themselves, do not show such weapon itself int type = weapon->get()->mBase->mData.mType; auto weaponClass = MWMechanics::getWeaponType(type)->mWeaponClass; if (weaponClass == ESM::WeaponType::Thrown) showHolsteredWeapons = false; const VFS::Path::Normalized mesh = weapon->getClass().getCorrectedModel(*weapon); if (mesh.empty()) return; const std::string_view boneName = getHolsteredWeaponBoneName(*weapon); if (boneName.empty()) return; // If the scabbard is not found, use the weapon mesh as fallback. const VFS::Path::Normalized scabbardName = addSuffixBeforeExtension(mesh, "_sh"); const bool isEnchanted = !weapon->getClass().getEnchantment(*weapon).empty(); if (!mResourceSystem->getVFS()->exists(scabbardName)) { if (showHolsteredWeapons) { const osg::Vec4f glowColor = isEnchanted ? weapon->getClass().getEnchantmentColor(*weapon) : osg::Vec4f(); mScabbard = attachMesh(mesh, boneName, isEnchanted ? &glowColor : nullptr); if (mScabbard && weaponClass == ESM::WeaponType::Ranged) resetControllers(mScabbard->getNode()); } return; } mScabbard = attachMesh(scabbardName, boneName); if (!mScabbard || !mScabbard->getNode()) return; if (weaponClass == ESM::WeaponType::Ranged) resetControllers(mScabbard->getNode()); SceneUtil::FindByNameVisitor findVisitor("Bip01 Weapon"); mScabbard->getNode()->accept(findVisitor); osg::Group* weaponNode = findVisitor.mFoundNode; 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()) { osg::ref_ptr fallbackNode = mResourceSystem->getSceneManager()->getInstance(mesh, weaponNode); if (weaponClass == ESM::WeaponType::Ranged) resetControllers(fallbackNode); } if (isEnchanted) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); mGlowUpdater = SceneUtil::addEnchantedGlow(weaponNode, mResourceSystem, glowColor); } } } void ActorAnimation::updateQuiver() { if (!Settings::game().mWeaponSheathing) 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->getType() != ESM::Weapon::sRecordId) return; std::string_view mesh = weapon->getClass().getModel(*weapon); std::string_view 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) { ammoCount = ammo->getCellRef().getCount(); osg::Group* throwingWeaponNode = getBoneByName(weaponType->mAttachBone); if (throwingWeaponNode && throwingWeaponNode->getNumChildren()) ammoCount--; suitableAmmo = true; } else { ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; ammoCount = ammo->getCellRef().getCount(); bool arrowAttached = isArrowAttached(); if (arrowAttached) ammoCount--; 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.getType() == ESM::Light::sRecordId) { const ESM::Light* light = item.get()->mBase; if (!(light->mData.mFlags & ESM::Light::Carry)) { addHiddenItemLight(item, light); } } 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->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); if (ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) updateQuiver(); } void ActorAnimation::itemRemoved(const MWWorld::ConstPtr& item, int /*count*/) { if (item.getType() == ESM::Light::sRecordId) { ItemLightMap::iterator iter = mItemLights.find(item); if (iter != mItemLights.end()) { 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->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); if (ammo != inv.end() && item.getCellRef().getRefId() == ammo->getCellRef().getRefId()) updateQuiver(); } void ActorAnimation::addHiddenItemLight(const MWWorld::ConstPtr& item, const ESM::Light* esmLight) { if (mItemLights.find(item) != mItemLights.end()) return; bool exterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); osg::Vec4f ambient(1, 1, 1, 1); osg::ref_ptr lightSource = SceneUtil::createLightSource(SceneUtil::LightCommon(*esmLight), Mask_Lighting, exterior, ambient); mInsert->addChild(lightSource); if (mLightListCallback && mPtr == MWMechanics::getPlayer()) mLightListCallback->getIgnoredLightSources().insert(lightSource.get()); mItemLights.insert(std::make_pair(item, lightSource)); } void ActorAnimation::removeHiddenItemLight(const MWWorld::ConstPtr& item) { 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); } void ActorAnimation::removeFromScene() { removeFromSceneImpl(); Animation::removeFromScene(); } void ActorAnimation::removeFromSceneImpl() { for (const auto& [k, v] : mItemLights) mInsert->removeChild(v); } } openmw-openmw-0.49.0/apps/openmw/mwrender/actoranimation.hpp000066400000000000000000000044461503074453300242440ustar00rootroot00000000000000#ifndef GAME_RENDER_ACTORANIMATION_H #define GAME_RENDER_ACTORANIMATION_H #include #include #include #include "../mwworld/containerstore.hpp" #include "animation.hpp" namespace osg { class Node; } namespace MWWorld { class ConstPtr; } namespace SceneUtil { class LightSource; class LightListCallback; } namespace MWRender { class ActorAnimation : public Animation, public MWWorld::ContainerStoreListener { public: ActorAnimation( const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem); virtual ~ActorAnimation(); void itemAdded(const MWWorld::ConstPtr& item, int count) override; void itemRemoved(const MWWorld::ConstPtr& item, int count) override; virtual bool isArrowAttached() const { return false; } bool useShieldAnimations() const override; bool updateCarriedLeftVisible(const int weaptype) const override; void removeFromScene() override; protected: osg::Group* getBoneByName(std::string_view boneName) const; void updateHolsteredWeapon(bool showHolsteredWeapons); void updateHolsteredShield(bool showCarriedLeft); void updateQuiver(); std::string getShieldMesh(const MWWorld::ConstPtr& shield, bool female) const; virtual std::string getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const; std::string_view getHolsteredWeaponBoneName(const MWWorld::ConstPtr& weapon); PartHolderPtr attachMesh( VFS::Path::NormalizedView model, std::string_view bonename, const osg::Vec4f* glowColor = nullptr); osg::ref_ptr attach( VFS::Path::NormalizedView model, std::string_view bonename, std::string_view bonefilter, bool isLight); PartHolderPtr mScabbard; PartHolderPtr mHolsteredShield; private: 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; ItemLightMap mItemLights; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/actorspaths.cpp000066400000000000000000000075401503074453300235600ustar00rootroot00000000000000#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()) { } ActorsPaths::~ActorsPaths() { if (mEnabled) disable(); } bool ActorsPaths::toggle() { if (mEnabled) disable(); else enable(); return mEnabled; } void ActorsPaths::update(const MWWorld::ConstPtr& actor, const std::deque& path, 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.mRef); if (group != mGroups.end()) mRootNode->removeChild(group->second.mNode); 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.mRef); if (group != mGroups.end()) { mRootNode->removeChild(group->second.mNode); mGroups.erase(group); } } void ActorsPaths::removeCell(const MWWorld::CellStore* const store) { for (auto it = mGroups.begin(); it != mGroups.end();) { if (it->second.mCell == store) { mRootNode->removeChild(it->second.mNode); it = mGroups.erase(it); } else ++it; } } void ActorsPaths::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { const auto it = mGroups.find(old.mRef); if (it == mGroups.end()) return; it->second.mCell = updated.mCell; } void ActorsPaths::enable() { 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.mNode); }); mEnabled = false; } } openmw-openmw-0.49.0/apps/openmw/mwrender/actorspaths.hpp000066400000000000000000000026331503074453300235630ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_AGENTSPATHS_H #define OPENMW_MWRENDER_AGENTSPATHS_H #include "apps/openmw/mwworld/ptr.hpp" #include #include #include namespace osg { class Group; class StateSet; } namespace DetourNavigator { struct Settings; struct AgentBounds; } namespace MWRender { class ActorsPaths { public: ActorsPaths(const osg::ref_ptr& root, bool enabled); ~ActorsPaths(); bool toggle(); void update(const MWWorld::ConstPtr& actor, const std::deque& path, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end, const DetourNavigator::Settings& settings); void remove(const MWWorld::ConstPtr& actor); void removeCell(const MWWorld::CellStore* const store); void updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated); void enable(); void disable(); private: 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/actorutil.cpp000066400000000000000000000035541503074453300232340ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/actorutil.hpp000066400000000000000000000006501503074453300232330ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/animation.cpp000066400000000000000000002267341503074453300232140ustar00rootroot00000000000000#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 "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/world.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 "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 { public: RemoveParticlesVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Node& node) override { if (dynamic_cast(&node)) mToRemove.emplace_back(&node); traverse(node); } void apply(osg::Drawable& drw) override { if (osgParticle::ParticleSystem* partsys = dynamic_cast(&drw)) mToRemove.emplace_back(partsys); } void remove() { for (osg::Node* node : mToRemove) { // FIXME: a Drawable might have more than one parent if (node->getNumParents()) node->getParent(0)->removeChild(node); } mToRemove.clear(); } private: std::vector> mToRemove; }; class DayNightCallback : public SceneUtil::NodeCallback { public: DayNightCallback() : mCurrentState(0) { } void operator()(osg::Switch* node, osg::NodeVisitor* nv) { unsigned int state = MWBase::Environment::get().getWorld()->getNightDayMode(); const unsigned int newState = node->getNumChildren() > state ? state : 0; if (newState != mCurrentState) { mCurrentState = newState; node->setSingleChildOn(mCurrentState); } traverse(node, nv); } private: unsigned int mCurrentState; }; class AddSwitchCallbacksVisitor : public osg::NodeVisitor { public: AddSwitchCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Switch& switchNode) override { if (switchNode.getName() == Constants::NightDayLabel) switchNode.addUpdateCallback(new DayNightCallback()); traverse(switchNode); } }; class HarvestVisitor : public osg::NodeVisitor { public: HarvestVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Switch& node) override { if (node.getName() == Constants::HerbalismLabel) { node.setSingleChildOn(1); } traverse(node); } }; 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) { float starttime = std::numeric_limits::max(); float stoptime = 0.0f; // Pick the last Loop Stop key and the last Loop Start key. // This is required because of broken text keys in AshVampire.nif. // It has *two* WalkForward: Loop Stop keys at different times, the first one is used for stopping playback // but the animation velocity calculation uses the second one. // 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()) { if (equalsParts(keyiter->second, groupname, ": start") || equalsParts(keyiter->second, groupname, ": loop start")) { starttime = keyiter->first; break; } ++keyiter; } keyiter = keys.rbegin(); while (keyiter != keys.rend()) { if (equalsParts(keyiter->second, groupname, ": stop")) stoptime = keyiter->first; else if (equalsParts(keyiter->second, groupname, ": loop stop")) { stoptime = keyiter->first; break; } ++keyiter; } 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 0.0f; } class GetExtendedBonesVisitor : public osg::NodeVisitor { public: GetExtendedBonesVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Node& node) override { if (SceneUtil::hasUserDescription(&node, "CustomBone")) { mFoundBones.emplace_back(&node, node.getParent(0)); return; } traverse(node); } std::vector> mFoundBones; }; class RemoveFinishedCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; RemoveFinishedCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) { } void apply(osg::Node& node) override { traverse(node); } void apply(osg::Group& group) override { traverse(group); osg::Callback* callback = group.getUpdateCallback(); if (callback) { // We should remove empty transformation nodes and finished callbacks here MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { if (vfxCallback->mFinished) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else mHasMagicEffects = true; } } } void apply(osg::MatrixTransform& node) override { traverse(node); } void apply(osg::Geometry&) override {} }; class RemoveCallbackVisitor : public SceneUtil::RemoveVisitor { public: bool mHasMagicEffects; RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) { } RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) { } void apply(osg::Node& node) override { traverse(node); } void apply(osg::Group& group) override { traverse(group); osg::Callback* callback = group.getUpdateCallback(); if (callback) { MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { bool toRemove = mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else mHasMagicEffects = true; } } } void apply(osg::MatrixTransform& node) override { traverse(node); } void apply(osg::Geometry&) override {} private: std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor { public: std::vector mCallbacks; FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } FindVfxCallbacksVisitor(std::string_view effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { } void apply(osg::Node& node) override { traverse(node); } void apply(osg::Group& group) override { osg::Callback* callback = group.getUpdateCallback(); if (callback) { MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { if (mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } } } traverse(group); } void apply(osg::MatrixTransform& node) override { traverse(node); } void apply(osg::Geometry&) override {} private: std::string_view mEffectId; }; void assignBoneBlendCallbackRecursive(MWRender::BoneAnimBlendController* controller, osg::Node* parent, bool isRoot) { // Attempt to cast node to an osgAnimation::Bone if (!isRoot && dynamic_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); // 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) { if (dynamic_cast(updateCb)) { osg::ref_ptr nextCb = updateCb->getNestedCallback(); bone->removeUpdateCallback(updateCb); updateCb = nextCb; } else { updateCb = updateCb->getNestedCallback(); } } // 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) { 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 { class TransparencyUpdater : public SceneUtil::StateSetUpdater { public: TransparencyUpdater(const float alpha) : mAlpha(alpha) { } 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); stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN); stateset->setRenderBinMode(osg::StateSet::OVERRIDE_RENDERBIN_DETAILS); // 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); } void apply(osg::StateSet* stateset, osg::NodeVisitor* /*nv*/) override { osg::Material* material = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); material->setAlpha(osg::Material::FRONT_AND_BACK, mAlpha); } private: float mAlpha; }; struct Animation::AnimSource { osg::ref_ptr mKeyframes; typedef std::map> ControllerMap; ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; osg::ref_ptr mAnimBlendRules; }; void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv) { traverse(node, nv); if (mFinished) return; double newTime = nv->getFrameStamp()->getSimulationTime(); if (mStartingTime == 0) { mStartingTime = newTime; return; } double duration = newTime - mStartingTime; mStartingTime = newTime; mParams.mAnimTime->addTime(duration); if (mParams.mAnimTime->getTime() >= mParams.mMaxControllerLength) { if (mParams.mLoop) { // Start from the beginning again; carry over the remainder // Not sure if this is actually needed, the controller function might already handle loops float remainder = mParams.mAnimTime->getTime() - mParams.mMaxControllerLength; mParams.mAnimTime->resetTime(remainder); } else { // Hide effect immediately node->setNodeMask(0); mFinished = true; } } } class ResetAccumRootCallback : public SceneUtil::NodeCallback { public: void operator()(osg::MatrixTransform* transform, osg::NodeVisitor* nv) { osg::Matrix mat = transform->getMatrix(); osg::Vec3f position = mat.getTrans(); position = osg::componentMultiply(mResetAxes, position); mat.setTrans(position); transform->setMatrix(mat); traverse(transform, nv); } void setAccumulate(const osg::Vec3f& accumulate) { // anything that accumulates (1.f) should be reset in the callback to (0.f) mResetAxes.x() = accumulate.x() != 0.f ? 0.f : 1.f; mResetAxes.y() = accumulate.y() != 0.f ? 0.f : 1.f; mResetAxes.z() = accumulate.z() != 0.f ? 0.f : 1.f; } private: osg::Vec3f mResetAxes; }; Animation::Animation( const MWWorld::Ptr& ptr, osg::ref_ptr parentNode, Resource::ResourceSystem* resourceSystem) : mInsert(std::move(parentNode)) , mSkeleton(nullptr) , mNodeMapCreated(false) , mPtr(ptr) , mResourceSystem(resourceSystem) , mAccumulate(1.f, 1.f, 0.f) , mTextKeyListener(nullptr) , mHeadYawRadians(0.f) , mHeadPitchRadians(0.f) , mUpperBodyYawRadians(0.f) , mLegsYawRadians(0.f) , mBodyPitchRadians(0.f) , mHasMagicEffects(false) , mAlpha(1.f) , mPlayScriptedOnly(false) , mRequiresBoneMap(false) { for (size_t i = 0; i < sNumBlendMasks; i++) mAnimationTimePtr[i] = std::make_shared(); mLightListCallback = new SceneUtil::LightListCallback; } Animation::~Animation() { removeFromSceneImpl(); } void Animation::setActive(int active) { if (mSkeleton) mSkeleton->setActive(static_cast(active)); } void Animation::updatePtr(const MWWorld::Ptr& ptr) { mPtr = ptr; } void Animation::setAccumulation(const osg::Vec3f& accum) { mAccumulate = accum; if (mResetAccumRootCallback) mResetAccumRootCallback->setAccumulate(mAccumulate); } // controllerName is used for Collada animated deforming models size_t Animation::detectBlendMask(const osg::Node* node, const std::string& controllerName) const { 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) { const std::string& name = node->getName(); for (size_t i = 1; i < sNumBlendMasks; i++) { if (name == sBlendMaskRoots[i] || controllerName == sBlendMaskRoots[i]) return i; } assert(node->getNumParents() > 0); node = node->getParent(0); } return 0; } const SceneUtil::TextKeyMap& Animation::AnimSource::getTextKeys() const { return mKeyframes->mTextKeys; } void Animation::loadAdditionalAnimations(VFS::Path::NormalizedView model, const std::string& baseModel) { constexpr VFS::Path::NormalizedView meshes("meshes/"); if (!model.value().starts_with(meshes.value())) return; std::string path(model.value()); constexpr VFS::Path::NormalizedView animations("animations/"); path.replace(0, meshes.value().size(), animations.value()); const std::string::size_type extensionStart = path.find_last_of(VFS::Path::extensionSeparator); if (extensionStart == std::string::npos) return; path.replace(extensionStart, path.size() - extensionStart, "/"); for (const VFS::Path::Normalized& name : mResourceSystem->getVFS()->getRecursiveDirectoryIterator(path)) { if (Misc::getFileExtension(name) == "kf") { addSingleAnimSource(name, baseModel); } } } void Animation::addAnimSource(std::string_view model, const std::string& baseModel) { VFS::Path::Normalized kfname(model); if (Misc::getFileExtension(kfname) == "nif") kfname.changeExtension("kf"); addSingleAnimSource(kfname, baseModel); if (Settings::game().mUseAdditionalAnimSources) loadAdditionalAnimations(kfname, baseModel); } std::shared_ptr Animation::addSingleAnimSource( const std::string& kfname, const std::string& baseModel) { if (!mResourceSystem->getVFS()->exists(kfname)) return nullptr; auto animsrc = std::make_shared(); animsrc->mKeyframes = mResourceSystem->getKeyframeManager()->get(VFS::Path::toNormalized(kfname)); if (!animsrc->mKeyframes || animsrc->mKeyframes->mTextKeys.empty() || animsrc->mKeyframes->mKeyframeControllers.empty()) return nullptr; const NodeMap& nodeMap = getNodeMap(); 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 << ")"; continue; } osg::Node* node = found->second; 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); cloned->setSource(mAnimationTimePtr[blendMask]); animsrc->mControllerMap[blendMask].insert(std::make_pair(bonename, cloned)); } 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) { // 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; } } // 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++) mAnimationTimePtr[i]->setTimePtr(std::shared_ptr()); mAccumCtrl = nullptr; mSupportedAnimations.clear(); mAnimSources.clear(); mAnimVelocities.clear(); } bool Animation::hasAnimation(std::string_view anim) const { return mSupportedAnimations.find(anim) != mSupportedAnimations.end(); } bool Animation::isLoopingAnimation(std::string_view group) const { // 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) { if (suffix.length() > suffixLength && group.ends_with(suffix)) { suffixLength = suffix.length(); } } group.remove_suffix(suffixLength); return loopingAnimations.count(group) > 0; } float Animation::getStartTime(const std::string& groupname) const { for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys(); const auto found = keys.findGroupStart(groupname); if (found != keys.end()) return found->first; } return -1.f; } float Animation::getTextKeyTime(std::string_view textKey) const { for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) { const SceneUtil::TextKeyMap& keys = (*iter)->getTextKeys(); for (auto iterKey = keys.begin(); iterKey != keys.end(); ++iterKey) { if (iterKey->second.starts_with(textKey)) return iterKey->first; } } return -1.f; } void Animation::handleTextKey(AnimState& state, std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) { std::string_view evt = key->second; if (evt.starts_with(groupname) && evt.substr(groupname.size()).starts_with(": ")) { size_t off = groupname.size() + 2; if (evt.substr(off) == "loop start") state.mLoopStartTime = key->first; else if (evt.substr(off) == "loop stop") state.mLoopStopTime = key->first; } try { if (mTextKeyListener != nullptr) mTextKeyListener->handleTextKey(groupname, key, map); } catch (std::exception& e) { Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } 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()) return; 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()) { if (stateiter->second.mPriority == priority && stateiter->first != groupname) mStates.erase(stateiter++); else ++stateiter; } if (foundstateiter != mStates.end()) { resetActiveGroups(); return; } /* Look in reverse; last-inserted source has priority. */ AnimState state; AnimSourceList::reverse_iterator iter(mAnimSources.rbegin()); for (; iter != mAnimSources.rend(); ++iter) { const SceneUtil::TextKeyMap& textkeys = (*iter)->getTextKeys(); if (reset(state, textkeys, groupname, start, stop, startpoint, loopfallback)) { state.mSource = *iter; state.mSpeedMult = speedmult; state.mLoopCount = loops; state.mPlaying = (state.getTime() < state.mStopTime); state.mPriority = priority; state.mBlendMask = blendMask; state.mAutoDisable = autodisable; 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()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } if (state.getTime() >= state.mLoopStopTime && state.mLoopCount > 0) { state.mLoopCount--; state.setTime(state.mLoopStartTime); state.mPlaying = true; if (state.getTime() >= state.mLoopStopTime) break; auto textkey = textkeys.lowerBound(state.getTime()); while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, groupname, textkey, textkeys); ++textkey; } } break; } } resetActiveGroups(); } 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) { if (groupend->second.starts_with(groupname) && groupend->second.compare(groupname.size(), 2, ": ") == 0) break; } auto startkey = groupend; while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": ", start)) ++startkey; if (startkey == keys.rend() && start == "loop start") { startkey = groupend; while (startkey != keys.rend() && !equalsParts(startkey->second, groupname, ": start")) ++startkey; } if (startkey == keys.rend()) return false; auto stopkey = groupend; 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()) return false; if (startkey->first > stopkey->first) return false; state.mStartTime = startkey->first; if (loopfallback) { state.mLoopStartTime = startkey->first; state.mLoopStopTime = stopkey->first; } else { state.mLoopStartTime = startkey->first; state.mLoopStopTime = std::numeric_limits::max(); } state.mStopTime = stopkey->first; 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. auto key = groupend; for (; key != startkey && key != keys.rend(); ++key) { if (key->first > state.getTime()) continue; if (equalsParts(key->second, groupname, ": loop start")) state.mLoopStartTime = key->first; else if (equalsParts(key->second, groupname, ": loop stop")) state.mLoopStopTime = key->first; } return true; } void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } const Animation::NodeMap& Animation::getNodeMap() const { if (!mNodeMapCreated && mObjectRoot) { // 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 for (auto it = mActiveControllers.begin(); it != mActiveControllers.end(); ++it) { osg::Node* node = it->first; node->removeUpdateCallback(it->second); // Should be no longer needed with OSG 3.4 it->second->setNestedCallback(nullptr); } mActiveControllers.clear(); mAccumCtrl = nullptr; 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) { if (!state->second.blendMaskContains(blendMask)) continue; 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); // 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) { osg::ref_ptr node = getNodeMap().at( it->first); // this should not throw, we already checked for the node existing in addAnimSource 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) { mAccumCtrl = it->second; // make sure reset is last in the chain of callbacks if (!mResetAccumRootCallback) { mResetAccumRootCallback = new ResetAccumRootCallback; mResetAccumRootCallback->setAccumulate(mAccumulate); } mAccumRoot->addUpdateCallback(mResetAccumRootCallback); mActiveControllers.emplace_back(mAccumRoot, mResetAccumRootCallback); } } } } addControllers(); } void Animation::adjustSpeedMult(const std::string& groupname, float speedmult) { AnimStateMap::iterator state(mStates.find(groupname)); if (state != mStates.end()) state->second.mSpeedMult = speedmult; } bool Animation::isPlaying(std::string_view groupname) const { AnimStateMap::const_iterator state(mStates.find(groupname)); if (state != mStates.end()) return state->second.mPlaying; return false; } 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 (loopcount) *loopcount = 0; return false; } if (complete) { 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 (loopcount) *loopcount = iter->second.mLoopCount; return true; } std::string_view Animation::getActiveGroup(BoneGroup boneGroup) const { if (auto timePtr = mAnimationTimePtr[boneGroup]->getTimePtr()) for (auto& state : mStates) if (state.second.mTime == timePtr) return state.first; return ""; } float Animation::getCurrentTime(std::string_view groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) return -1.f; return iter->second.getTime(); } void Animation::disable(std::string_view groupname) { AnimStateMap::iterator iter = mStates.find(groupname); if (iter != mStates.end()) mStates.erase(iter); resetActiveGroups(); } float Animation::getVelocity(std::string_view groupname) const { if (!mAccumRoot) return 0.0f; std::map::const_iterator found = mAnimVelocities.find(groupname); if (found != mAnimVelocities.end()) return found->second; // Look in reverse; last-inserted source has priority. AnimSourceList::const_reverse_iterator animsrc(mAnimSources.rbegin()); for (; animsrc != mAnimSources.rend(); ++animsrc) { const SceneUtil::TextKeyMap& keys = (*animsrc)->getTextKeys(); if (keys.hasGroupStart(groupname)) break; } if (animsrc == mAnimSources.rend()) return 0.0f; float velocity = 0.0f; 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) { if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName())) { velocity = calcAnimVelocity(keys, it->second, mAccumulate, groupname); break; } } // If there's no velocity, keep looking if (!(velocity > 1.0f)) { AnimSourceList::const_reverse_iterator animiter = mAnimSources.rbegin(); while (*animiter != *animsrc) ++animiter; while (!(velocity > 1.0f) && ++animiter != mAnimSources.rend()) { 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) { if (Misc::StringUtils::ciEqual(it->first, mAccumRoot->getName())) { velocity = calcAnimVelocity(keys2, it->second, mAccumulate, groupname); break; } } } } mAnimVelocities.emplace(groupname, velocity); return velocity; } void Animation::updatePosition(float oldtime, float newtime, osg::Vec3f& position) { // Get the difference from the last update, and move the position osg::Vec3f off = osg::componentMultiply(mAccumCtrl->getTranslation(newtime), mAccumulate); position += off - osg::componentMultiply(mAccumCtrl->getTranslation(oldtime), mAccumulate); } osg::Vec3f Animation::runAnimation(float duration) { osg::Vec3f movement(0.f, 0.f, 0.f); AnimStateMap::iterator stateiter = mStates.begin(); while (stateiter != mStates.end()) { AnimState& state = stateiter->second; if (mPlayScriptedOnly && !state.mPriority.contains(MWMechanics::Priority_Scripted)) { ++stateiter; continue; } const SceneUtil::TextKeyMap& textkeys = state.mSource->getTextKeys(); auto textkey = textkeys.upperBound(state.getTime()); float timepassed = duration * state.mSpeedMult; while (state.mPlaying) { if (!state.shouldLoop()) { float targetTime = state.getTime() + timepassed; if (textkey == textkeys.end() || textkey->first > targetTime) { 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()) updatePosition(state.getTime(), textkey->first, movement); state.setTime(textkey->first); } state.mPlaying = (state.getTime() < state.mStopTime); timepassed = targetTime - state.getTime(); while (textkey != textkeys.end() && textkey->first <= state.getTime()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } } 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()) { handleTextKey(state, stateiter->first, textkey, textkeys); ++textkey; } if (state.getTime() >= state.mLoopStopTime) break; } if (timepassed <= 0.0f) break; } if (!state.mPlaying && state.mAutoDisable) { mStates.erase(stateiter++); resetActiveGroups(); } else ++stateiter; } updateEffects(); const float epsilon = 0.001f; float yawOffset = 0; if (mRootController) { bool enable = std::abs(mLegsYawRadians) > epsilon || std::abs(mBodyPitchRadians) > epsilon; mRootController->setEnabled(enable); if (enable) { 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) { float yaw = mUpperBodyYawRadians - yawOffset; bool enable = std::abs(yaw) > epsilon; mSpineController->setEnabled(enable); if (enable) { mSpineController->setRotate(osg::Quat(yaw, osg::Vec3f(0, 0, 1))); yawOffset = mUpperBodyYawRadians; } } if (mHeadController) { float yaw = mHeadYawRadians - yawOffset; 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))); } return movement; } void Animation::setLoopingEnabled(std::string_view groupname, bool enabled) { AnimStateMap::iterator state(mStates.find(groupname)); if (state != mStates.end()) state->second.mLoopingEnabled = enabled; } 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 GetExtendedBonesVisitor getBonesVisitor; sheathSkeleton->accept(getBonesVisitor); for (auto& nodePair : getBonesVisitor.mFoundBones) { SceneUtil::FindByNameVisitor findVisitor(nodePair.second->getName()); baseNode->accept(findVisitor); osg::Group* sheathParent = findVisitor.mFoundNode; if (sheathParent) { osg::Node* copy = static_cast(nodePair.first->clone(osg::CopyOp::DEEP_COPY_NODES)); sheathParent->addChild(copy); } } } void injectCustomBones( osg::ref_ptr& node, const std::string& model, Resource::ResourceSystem* resourceSystem) { if (model.empty()) return; std::string animationPath = model; if (animationPath.find("meshes") == 0) { animationPath.replace(0, 6, "animations"); } animationPath.replace(animationPath.size() - 4, 4, "/"); for (const VFS::Path::Normalized& name : resourceSystem->getVFS()->getRecursiveDirectoryIterator(animationPath)) { if (Misc::getFileExtension(name) == "nif") loadBonesFromFile(node, name, resourceSystem); } } 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; static Cache cache; Cache::iterator found = cache.find(model); if (found == cache.end()) { osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { injectCustomBones(created, defaultSkeleton, resourceSystem); injectCustomBones(created, model, resourceSystem); } SceneUtil::CleanObjectRootVisitor removeDrawableVisitor; created->accept(removeDrawableVisitor); removeDrawableVisitor.remove(); cache.insert(std::make_pair(model, created)); return sceneMgr->getInstance(created); } else return sceneMgr->getInstance(found->second); } else { osg::ref_ptr created = sceneMgr->getInstance(VFS::Path::toNormalized(model)); if (inject) { injectCustomBones(created, defaultSkeleton, resourceSystem); injectCustomBones(created, model, resourceSystem); } return created; } } void Animation::setObjectRoot(const std::string& model, bool forceskeleton, bool baseonly, bool isCreature) { osg::ref_ptr previousStateset; if (mObjectRoot) { if (mLightListCallback) mObjectRoot->removeCullCallback(mLightListCallback); if (mTransparencyUpdater) mObjectRoot->removeCullCallback(mTransparencyUpdater); previousStateset = mObjectRoot->getStateSet(); mObjectRoot->getParent(0)->removeChild(mObjectRoot); } mObjectRoot = nullptr; mSkeleton = nullptr; mNodeMap.clear(); mNodeMapCreated = false; mActiveControllers.clear(); mAccumRoot = nullptr; mAccumCtrl = nullptr; std::string defaultSkeleton; bool inject = false; if (Settings::game().mUseAdditionalAnimSources && mPtr.getClass().isActor()) { if (isCreature) { MWWorld::LiveCellRef* ref = mPtr.get(); if (ref->mBase->mFlags & ESM::Creature::Bipedal) { defaultSkeleton = Settings::models().mXbaseanim.get().value(); inject = true; } } else { inject = true; 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().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( VFS::Path::toNormalized(getActorSkeleton(firstPerson, isFemale, isBeast, werewolf)), mResourceSystem->getVFS()); } } } if (!forceskeleton) { osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); mInsert->addChild(created); mObjectRoot = created->asGroup(); if (!mObjectRoot) { mInsert->removeChild(created); mObjectRoot = new osg::Group; mObjectRoot->addChild(created); mInsert->addChild(mObjectRoot); } osg::ref_ptr skel = dynamic_cast(mObjectRoot.get()); if (skel) mSkeleton = skel.get(); } else { osg::ref_ptr created = getModelInstance(mResourceSystem, model, baseonly, inject, defaultSkeleton); osg::ref_ptr skel = dynamic_cast(created.get()); if (!skel) { skel = new SceneUtil::Skeleton; skel->addChild(created); } mSkeleton = skel.get(); mObjectRoot = skel; 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); if (isCreature) { SceneUtil::RemoveTriBipVisitor removeTriBipVisitor; mObjectRoot->accept(removeTriBipVisitor); removeTriBipVisitor.remove(); } if (!mLightListCallback) mLightListCallback = new SceneUtil::LightListCallback; mObjectRoot->addCullCallback(mLightListCallback); if (mTransparencyUpdater) mObjectRoot->addCullCallback(mTransparencyUpdater); } osg::Group* Animation::getObjectRoot() { return mObjectRoot.get(); } osg::Group* Animation::getOrCreateObjectRoot() { if (mObjectRoot) return mObjectRoot.get(); mObjectRoot = new osg::Group; mInsert->addChild(mObjectRoot); return mObjectRoot.get(); } void Animation::addSpellCastGlow(const osg::Vec4f& color, float glowDuration) { if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true))) { if (mGlowUpdater && mGlowUpdater->isDone()) mObjectRoot->removeUpdateCallback(mGlowUpdater); if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater()) { mGlowUpdater->setColor(color); mGlowUpdater->setDuration(glowDuration); } else if (mObjectRoot) mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration); } } 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_Lighting, exterior); mExtraLightSource->setActorFade(mAlpha); } void Animation::addEffect(std::string_view model, std::string_view effectId, bool loop, std::string_view bonename, std::string_view texture, bool useAmbientLight) { if (!mObjectRoot.get()) return; // Early out if we already have this effect FindVfxCallbacksVisitor visitor(effectId); mInsert->accept(visitor); for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { UpdateVfxCallback* callback = *it; if (loop && !callback->mFinished && callback->mParams.mLoop && callback->mParams.mBoneName == bonename) return; } EffectParams params; params.mModelName = model; osg::ref_ptr parentNode; if (bonename.empty()) parentNode = mInsert; else { NodeMap::const_iterator found = getNodeMap().find(bonename); if (found == getNodeMap().end()) throw std::runtime_error("Can't find bone " + std::string{ bonename }); parentNode = found->second; } osg::ref_ptr trans = new SceneUtil::PositionAttitudeTransform; if (!mPtr.getClass().isNpc()) { 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(VFS::Path::toNormalized(model), trans); if (useAmbientLight) { // 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); 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::make_shared(); trans->addUpdateCallback(new UpdateVfxCallback(params)); 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); } void Animation::removeEffect(std::string_view effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); visitor.remove(); mHasMagicEffects = visitor.mHasMagicEffects; } void Animation::removeEffects() { removeEffect(""); } std::vector Animation::getLoopingEffects() const { if (!mHasMagicEffects) return {}; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); 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() { // We do not need to visit scene every frame. // We can use a bool flag to check in spellcasting effect found. if (!mHasMagicEffects) return; // TODO: objects without animation still will have // transformation nodes with finished callbacks RemoveFinishedCallbackVisitor visitor; mInsert->accept(visitor); visitor.remove(); mHasMagicEffects = visitor.mHasMagicEffects; } bool Animation::upperBodyReady() const { 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))) return false; } return true; } const osg::Node* Animation::getNode(std::string_view name) const { NodeMap::const_iterator found = getNodeMap().find(name); if (found == getNodeMap().end()) return nullptr; else return found->second; } void Animation::setAlpha(float alpha) { if (alpha == mAlpha || !mObjectRoot) return; mAlpha = alpha; // TODO: we use it to fade actors away too, but it would be nice to have a dithering shader instead. if (alpha != 1.f) { if (mTransparencyUpdater == nullptr) { mTransparencyUpdater = new TransparencyUpdater(alpha); mObjectRoot->addCullCallback(mTransparencyUpdater); } else mTransparencyUpdater->setAlpha(alpha); } else { mObjectRoot->removeCullCallback(mTransparencyUpdater); mTransparencyUpdater = nullptr; } if (mExtraLightSource) mExtraLightSource->setActorFade(alpha); } void Animation::setLightEffect(float effect) { if (effect == 0) { if (mGlowLight) { mInsert->removeChild(mGlowLight); mGlowLight = nullptr; } } else { // 1 pt of Light magnitude corresponds to 1 foot of radius float radius = effect * std::ceil(Constants::UnitsPerFoot); // Arbitrary multiplier used to make the obvious cut-off less obvious float cutoffMult = 3; if (!mGlowLight || (radius * cutoffMult) != mGlowLight->getRadius()) { if (mGlowLight) { mInsert->removeChild(mGlowLight); 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)); bool isExterior = mPtr.isInCell() && mPtr.getCell()->getCell()->isExterior(); SceneUtil::configureLight(light, radius, isExterior); mGlowLight = new SceneUtil::LightSource; mGlowLight->setNodeMask(Mask_Lighting); mInsert->addChild(mGlowLight); mGlowLight->setLight(light); } mGlowLight->setRadius(radius * cutoffMult); } } void Animation::addControllers() { mHeadController = addRotateController("bip01 head"); mSpineController = addRotateController("bip01 spine1"); mRootController = addRotateController("bip01"); } osg::ref_ptr Animation::addRotateController(std::string_view bone) { auto iter = getNodeMap().find(bone); if (iter == getNodeMap().end()) return nullptr; osg::MatrixTransform* node = iter->second; bool foundKeyframeCtrl = false; osg::Callback* cb = node->getUpdateCallback(); while (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; osg::ref_ptr controller(new RotateController(mObjectRoot.get())); node->addUpdateCallback(controller); mActiveControllers.emplace_back(node, controller); return controller; } void Animation::setHeadPitch(float pitchRadians) { mHeadPitchRadians = pitchRadians; } void Animation::setHeadYaw(float yawRadians) { mHeadYawRadians = yawRadians; } float Animation::getHeadPitch() const { return mHeadPitchRadians; } float Animation::getHeadYaw() const { 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*) { if (mTimePtr) return *mTimePtr; return 0.f; } float EffectAnimationTime::getValue(osg::NodeVisitor*) { return mTime; } void EffectAnimationTime::addTime(float duration) { mTime += duration; } void EffectAnimationTime::resetTime(float time) { mTime = time; } float EffectAnimationTime::getTime() const { return mTime; } // -------------------------------------------------------------------------------- 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()) { setObjectRoot(model, false, false, false); if (animated) addAnimSource(model, model); if (!ptr.getClass().getEnchantment(ptr).empty()) mGlowUpdater = SceneUtil::addEnchantedGlow( mObjectRoot, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr)); } 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) { RemoveParticlesVisitor visitor; mObjectRoot->accept(visitor); visitor.remove(); } if (Settings::game().mDayNightSwitches && SceneUtil::hasUserDescription(mObjectRoot, Constants::NightDayLabel)) { AddSwitchCallbacksVisitor visitor; mObjectRoot->accept(visitor); } if (Settings::game().mGraphicHerbalism && ptr.getRefData().getCustomData() != nullptr && ObjectAnimation::canBeHarvested()) { const MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); if (!store.hasVisibleItems()) { HarvestVisitor visitor; mObjectRoot->accept(visitor); } } } bool ObjectAnimation::canBeHarvested() const { if (mPtr.getType() != ESM::Container::sRecordId) return false; const MWWorld::LiveCellRef* ref = mPtr.get(); if (!(ref->mBase->mFlags & ESM::Container::Organic)) return false; return SceneUtil::hasUserDescription(mObjectRoot, Constants::HerbalismLabel); } // ------------------------------ PartHolder::PartHolder(osg::ref_ptr node) : mNode(std::move(node)) { } PartHolder::~PartHolder() { if (mNode.get() && !mNode->getNumParents()) 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"; mNode->getParent(0)->removeChild(mNode); } } } openmw-openmw-0.49.0/apps/openmw/mwrender/animation.hpp000066400000000000000000000505761503074453300232200ustar00rootroot00000000000000#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 #include namespace ESM { struct Light; struct MagicEffect; } namespace Resource { class ResourceSystem; } namespace SceneUtil { class KeyframeHolder; class KeyframeController; class LightSource; class LightListCallback; class Skeleton; struct LightCommon; } namespace MWRender { class ResetAccumRootCallback; class RotateController; class TransparencyUpdater; using ActiveControllersVector = std::vector, osg::ref_ptr>>; class EffectAnimationTime : public SceneUtil::ControllerSource { private: float mTime; public: float getValue(osg::NodeVisitor* nv) override; void addTime(float duration); void resetTime(float time); float getTime() const; EffectAnimationTime() : mTime(0) { } }; /// @brief Detaches the node from its parent when the object goes out of scope. class PartHolder { public: PartHolder(osg::ref_ptr node); ~PartHolder(); const osg::ref_ptr& getNode() const { return mNode; } private: osg::ref_ptr mNode; void operator=(const PartHolder&); PartHolder(const PartHolder&); }; using PartHolderPtr = std::unique_ptr; 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; }; class Animation : public osg::Referenced { public: using BlendMask = MWRender::BlendMask; using BoneGroup = MWRender::BoneGroup; using AnimPriority = MWRender::AnimPriority; class TextKeyListener { public: virtual void handleTextKey( std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map) = 0; virtual ~TextKeyListener() = default; }; void setTextKeyListener(TextKeyListener* listener); virtual bool updateCarriedLeftVisible(const int weaptype) const { return false; } typedef std::unordered_map, Misc::StringUtils::CiHash, Misc::StringUtils::CiEqual> NodeMap; protected: class AnimationTime : public SceneUtil::ControllerSource { private: std::shared_ptr mTimePtr; public: void setTimePtr(std::shared_ptr time) { mTimePtr = std::move(time); } std::shared_ptr getTimePtr() const { return mTimePtr; } float getValue(osg::NodeVisitor* nv) override; }; class NullAnimationTime : public SceneUtil::ControllerSource { public: float getValue(osg::NodeVisitor* nv) override { return 0.f; } }; struct AnimSource; struct AnimState { std::shared_ptr mSource; float mStartTime = 0; float mLoopStartTime = 0; float mLoopStopTime = 0; float mStopTime = 0; std::shared_ptr mTime = std::make_shared(0); float mSpeedMult = 1; bool mPlaying = false; bool mLoopingEnabled = true; uint32_t mLoopCount = 0; AnimPriority mPriority{ 0 }; int mBlendMask = 0; bool mAutoDisable = true; std::string mGroupname; std::string mStartKey; 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; } }; typedef std::map> AnimStateMap; AnimStateMap mStates; typedef std::vector> AnimSourceList; AnimSourceList mAnimSources; std::unordered_set mSupportedAnimations; osg::ref_ptr mInsert; osg::ref_ptr mObjectRoot; SceneUtil::Skeleton* mSkeleton; // The node expected to accumulate movement during movement animations. osg::ref_ptr mAccumRoot; // The controller animating that node. osg::ref_ptr mAccumCtrl; // Used to reset the position of the accumulation root every frame - the movement should be applied to the // physics system osg::ref_ptr mResetAccumRootCallback; // 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; // Keep track of the animation controllers for easy access std::map, osg::ref_ptr> mAnimBlendControllers; std::map, osg::ref_ptr> mBoneAnimBlendControllers; std::shared_ptr mAnimationTimePtr[sNumBlendMasks]; mutable NodeMap mNodeMap; mutable bool mNodeMapCreated; MWWorld::Ptr mPtr; Resource::ResourceSystem* mResourceSystem; osg::Vec3f mAccumulate; TextKeyListener* mTextKeyListener; osg::ref_ptr mHeadController; osg::ref_ptr mSpineController; osg::ref_ptr mRootController; float mHeadYawRadians; float mHeadPitchRadians; float mUpperBodyYawRadians; float mLegsYawRadians; float mBodyPitchRadians; osg::ref_ptr addRotateController(std::string_view bone); bool mHasMagicEffects; osg::ref_ptr mGlowLight; osg::ref_ptr mGlowUpdater; osg::ref_ptr mTransparencyUpdater; osg::ref_ptr mExtraLightSource; float mAlpha; mutable std::map> mAnimVelocities; osg::ref_ptr mLightListCallback; bool mPlayScriptedOnly; bool mRequiresBoneMap; const NodeMap& getNodeMap() const; /* 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(); size_t detectBlendMask(const osg::Node* node, const std::string& controllerName) const; /* 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); /* 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); void handleTextKey(AnimState& state, std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map); /** 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); void loadAdditionalAnimations(VFS::Path::NormalizedView model, const std::string& baseModel); /** 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); /** Adds an additional light to the given node using the specified ESM record. */ void addExtraLight(osg::ref_ptr parent, const SceneUtil::LightCommon& 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(); void removeFromSceneImpl(); 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); 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 * @param useAmbientLight attach white ambient light to the root VFX node of the scenegraph (Morrowind * default) * @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 = {}, bool useAmbientLight = true); 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(); 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 SceneUtil::NodeCallback { public: UpdateVfxCallback(EffectParams& params) : mFinished(false) , mParams(params) , mStartingTime(0) { } bool mFinished; EffectParams mParams; void operator()(osg::Node* node, osg::NodeVisitor* nv); private: double mStartingTime; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/animationpriority.hpp000066400000000000000000000022151503074453300250050ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/animblendcontroller.cpp000066400000000000000000000345361503074453300252670ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/animblendcontroller.hpp000066400000000000000000000112501503074453300252600ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/blendmask.hpp000066400000000000000000000010501503074453300231600ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/bonegroup.hpp000066400000000000000000000004101503074453300232170ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/bulletdebugdraw.cpp000066400000000000000000000175311503074453300244020ustar00rootroot00000000000000#include #include #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(std::move(parentNode)) , mWorld(world) { DebugDrawer::setDebugMode(debugMode); } void DebugDrawer::createGeometry() { 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 (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(); } void DebugDrawer::step() { 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) { mLinesVertices->push_back(Misc::Convert::toOsg(from)); mLinesVertices->push_back(Misc::Convert::toOsg(to)); mLinesColors->push_back({ 1, 1, 1, 1 }); mLinesColors->push_back({ 1, 1, 1, 1 }); #if BT_BULLET_VERSION < 317 size_t size = mLinesVertices->size(); if (size >= 6 && (*mLinesVertices)[size - 1] == (*mLinesVertices)[size - 6] && (*mLinesVertices)[size - 2] == (*mLinesVertices)[size - 3] && (*mLinesVertices)[size - 4] == (*mLinesVertices)[size - 5]) { mTrisVertices->push_back(mLinesVertices->back()); mLinesVertices->pop_back(); mLinesColors->pop_back(); mTrisVertices->push_back(mLinesVertices->back()); mLinesVertices->pop_back(); mLinesColors->pop_back(); mLinesVertices->pop_back(); mLinesColors->pop_back(); mTrisVertices->push_back(mLinesVertices->back()); mLinesVertices->pop_back(); mLinesColors->pop_back(); mLinesVertices->pop_back(); mLinesColors->pop_back(); mLinesVertices->pop_back(); mLinesColors->pop_back(); } #endif } void DebugDrawer::drawTriangle( const btVector3& v0, const btVector3& v1, const btVector3& v2, const btVector3& color, btScalar) { mTrisVertices->push_back(Misc::Convert::toOsg(v0)); mTrisVertices->push_back(Misc::Convert::toOsg(v1)); mTrisVertices->push_back(Misc::Convert::toOsg(v2)); } void DebugDrawer::addCollision(const btVector3& orig, const btVector3& normal) { mCollisionViews.emplace_back(orig, normal); } void DebugDrawer::showCollisions() { const auto now = std::chrono::steady_clock::now(); for (auto& [from, to, created] : mCollisionViews) { 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(), [&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::reportErrorWarning(const char* warningString) { Log(Debug::Warning) << warningString; } void DebugDrawer::setDebugMode(int isOn) { mDebugOn = (isOn != 0); if (!mDebugOn) destroyGeometry(); else createGeometry(); } int DebugDrawer::getDebugMode() const { return mDebugOn; } } openmw-openmw-0.49.0/apps/openmw/mwrender/bulletdebugdraw.hpp000066400000000000000000000050661503074453300244070ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_BULLETDEBUGDRAW_H #define OPENMW_MWRENDER_BULLETDEBUGDRAW_H #include #include #include #include #include #include class btCollisionWorld; namespace osg { class Group; class Geometry; } namespace MWRender { class DebugDrawer : public btIDebugDraw { 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/camera.cpp000066400000000000000000000350541503074453300224560ustar00rootroot00000000000000#include "camera.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/refdata.hpp" #include "../mwmechanics/movement.hpp" #include "../mwphysics/raycasting.hpp" #include "npcanimation.hpp" namespace { class UpdateRenderCameraCallback : public SceneUtil::NodeCallback { public: UpdateRenderCameraCallback(MWRender::Camera* cam) : mCamera(cam) { } 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); } private: MWRender::Camera* mCamera; }; } namespace MWRender { 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) { mUpdateCallback = new UpdateRenderCameraCallback(this); mCamera->addUpdateCallback(mUpdateCallback); } Camera::~Camera() { mCamera->removeUpdateCallback(mUpdateCallback); } osg::Vec3d Camera::calculateTrackedPosition() const { if (!mTrackingNode) return osg::Vec3d(); osg::NodePathList nodepaths = mTrackingNode->getParentalNodePaths(); if (nodepaths.empty()) return osg::Vec3d(); osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3d res = worldMat.getTrans(); if (mMode != Mode::FirstPerson) res.z() += mHeight * mHeightScale; return res; } osg::Vec3d Camera::getFocalPointOffset() const { osg::Vec3d offset; offset.x() = mFocalPointCurrentOffset.x() * cos(mYaw); offset.y() = mFocalPointCurrentOffset.x() * sin(mYaw); offset.z() = mFocalPointCurrentOffset.y(); return offset; } void Camera::updateCamera(osg::Camera* cam) { osg::Quat orient = getOrient(); osg::Vec3d forward = orient * osg::Vec3d(0, 1, 0); osg::Vec3d up = orient * osg::Vec3d(0, 0, 1); osg::Vec3d pos = mPosition; if (mMode == Mode::FirstPerson) { // 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); } cam->setViewMatrixAsLookAt(pos, pos + forward, up); mViewMatrix = cam->getViewMatrix(); mProjectionMatrix = cam->getProjectionMatrix(); } void Camera::update(float duration, bool paused) { 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() && mShowCrosshair); if (!paused) updateFocalPointOffset(duration); updatePosition(); } 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() { mTrackedPosition = calculateTrackedPosition(); if (mMode == Mode::Static) return; if (mMode == Mode::FirstPerson) { mPosition = calculateFirstPersonPosition(mTrackedPosition); mCameraDistance = 0; return; } 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 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, mCollisionType); if (result.mHit) { double adjustmentCoef = -(result.mHitPos + result.mHitNormal * focalObstacleLimit - focal).length() / offsetLen; focal += focalOffset * std::max(-1.0, adjustmentCoef); } } // 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::setMode(Mode newMode, bool force) { if (mMode == newMode) { mQueuedMode = std::nullopt; return; } 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) { instantTransition(); mProcessViewChange = true; } } void Camera::setFocalPointTargetOffset(const osg::Vec2d& v) { mFocalPointTargetOffset = v; mPreviousTransitionSpeed = mFocalPointTransitionSpeed; mPreviousTransitionInfluence = 1.0f; } void Camera::updateFocalPointOffset(float duration) { if (duration <= 0) return; if (mSkipFocalPointTransition) { mSkipFocalPointTransition = false; mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; mFocalPointCurrentOffset = mFocalPointTargetOffset; } osg::Vec2d oldOffset = mFocalPointCurrentOffset; if (mPreviousTransitionInfluence > 0) { mFocalPointCurrentOffset -= mPreviousExtraOffset; mPreviousExtraOffset = mPreviousExtraOffset / mPreviousTransitionInfluence + mPreviousTransitionSpeed * duration; mPreviousTransitionInfluence = std::max(0.f, mPreviousTransitionInfluence - duration * mFocalPointTransitionSpeedCoef); mPreviousExtraOffset *= mPreviousTransitionInfluence; mFocalPointCurrentOffset += mPreviousExtraOffset; } osg::Vec2d delta = mFocalPointTargetOffset - mFocalPointCurrentOffset; if (delta.length2() > 0) { float coef = duration * (1.0 + 5.0 / delta.length()) * mFocalPointTransitionSpeedCoef * (1.0f - mPreviousTransitionInfluence); mFocalPointCurrentOffset += delta * std::min(coef, 1.0f); } else { mPreviousExtraOffset = osg::Vec2d(); mPreviousTransitionInfluence = 0.f; } mFocalPointTransitionSpeed = (mFocalPointCurrentOffset - oldOffset) / duration; } void Camera::toggleViewMode(bool force) { setMode(mFirstPersonView ? Mode::ThirdPerson : Mode::FirstPerson, force); } bool Camera::toggleVanityMode(bool enable) { if (!enable) 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)); } void Camera::setYaw(float angle, bool force) { if (!mLockYaw || force) mYaw = Misc::normalizeAngle(angle); if (force) mLockYaw = true; } void Camera::setPitch(float angle, bool force) { const float epsilon = 0.000001f; float limit = static_cast(osg::PI_2) - epsilon; if (!mLockPitch || force) mPitch = std::clamp(angle, -limit, limit); if (force) mLockPitch = true; } void Camera::setStaticPosition(const osg::Vec3d& pos) { if (mMode != Mode::Static) throw std::runtime_error("setStaticPosition can be used only if camera is in Static mode"); mPosition = pos; } void Camera::setAnimation(NpcAnimation* anim) { mAnimation = anim; mProcessViewChange = true; } void Camera::processViewChange() { if (mTrackingPtr.isEmpty()) return; if (mMode == Mode::FirstPerson) { mAnimation->setViewMode(NpcAnimation::VM_FirstPerson); mTrackingNode = mAnimation->getNode("Camera"); if (!mTrackingNode) mTrackingNode = mAnimation->getNode("Head"); mHeightScale = 1.f; } else { mAnimation->setViewMode(NpcAnimation::VM_Normal); SceneUtil::PositionAttitudeTransform* transform = mTrackingPtr.getRefData().getBaseNode(); mTrackingNode = transform; if (transform) mHeightScale = transform->getScale().z(); else mHeightScale = 1.f; } mProcessViewChange = false; } void Camera::applyDeferredPreviewRotationToPlayer(float dt) { if (mMode != Mode::ThirdPerson || mTrackingPtr.isEmpty()) return; osg::Vec3f rot = mDeferredRotation; float delta = rot.normalize(); delta = std::min(delta, (delta + 1.f) * 3 * dt); rot *= delta; mDeferredRotation -= rot; if (mDeferredRotationDisabled) { mDeferredRotationDisabled = delta > 0.0001; rotateCameraToTrackingPtr(); return; } auto& movement = mTrackingPtr.getClass().getMovementSettings(mTrackingPtr); movement.mRotation[0] += rot.x(); movement.mRotation[1] += rot.y(); movement.mRotation[2] += rot.z(); if (std::abs(mDeferredRotation.z()) > 0.0001) { float s = std::sin(mDeferredRotation.z()); 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[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()); } void Camera::instantTransition() { mSkipFocalPointTransition = true; mDeferredRotationDisabled = false; mDeferredRotation = osg::Vec3f(); rotateCameraToTrackingPtr(); } void Camera::calculateDeferredRotation() { if (mMode == Mode::Static) { mDeferredRotation = osg::Vec3f(); return; } MWWorld::Ptr ptr = mTrackingPtr; if (mMode == Mode::Preview || mMode == Mode::Vanity || ptr.isEmpty()) return; if (mFirstPersonView) { instantTransition(); return; } mDeferredRotation.x() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[0] - mPitch); mDeferredRotation.z() = Misc::normalizeAngle(-ptr.getRefData().getPosition().rot[2] - mYaw); if (!mDeferredRotationAllowed) mDeferredRotationDisabled = true; } } openmw-openmw-0.49.0/apps/openmw/mwrender/camera.hpp000066400000000000000000000136071503074453300224630ustar00rootroot00000000000000#ifndef GAME_MWRENDER_CAMERA_H #define GAME_MWRENDER_CAMERA_H #include #include #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Camera; class Callback; class Node; } namespace MWRender { class NpcAnimation; /// \brief Camera control class Camera { public: enum class Mode : int { Static = 0, FirstPerson = 1, ThirdPerson = 2, Vanity = 3, Preview = 4 }; Camera(osg::Camera* camera); ~Camera(); /// Attach camera to object void attachTo(const MWWorld::Ptr& ptr) { mTrackingPtr = ptr; } MWWorld::Ptr getTrackingPtr() const { return mTrackingPtr; } void setFocalPointTransitionSpeed(float v) { mFocalPointTransitionSpeedCoef = v; } float getFocalPointTransitionSpeed() const { return mFocalPointTransitionSpeedCoef; } void setFocalPointTargetOffset(const osg::Vec2d& v); osg::Vec2d getFocalPointTargetOffset() const { return mFocalPointTargetOffset; } void instantTransition(); void showCrosshair(bool v) { mShowCrosshair = v; } /// Update the view matrix of \a cam void updateCamera(osg::Camera* cam); /// Reset to defaults void reset() { setMode(Mode::FirstPerson); } void rotateCameraToTrackingPtr(); float getPitch() const { return mPitch; } float getYaw() const { return mYaw; } float getRoll() const { return mRoll; } void setPitch(float angle, bool force = false); void setYaw(float angle, bool force = false); void setRoll(float angle) { mRoll = angle; } float getExtraPitch() const { return mExtraPitch; } float getExtraYaw() const { return mExtraYaw; } float getExtraRoll() const { return mExtraRoll; } void setExtraPitch(float angle) { mExtraPitch = angle; } void setExtraYaw(float angle) { mExtraYaw = angle; } void setExtraRoll(float angle) { mExtraRoll = angle; } osg::Quat getOrient() const; /// @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; } /// \brief Lowers the camera for sneak. void setSneakOffset(float offset); void processViewChange(); void update(float duration, bool paused = false); float getCameraDistance() const { return mCameraDistance; } void setPreferredCameraDistance(float v) { mPreferredCameraDistance = v; } void setAnimation(NpcAnimation* anim); osg::Vec3d getTrackedPosition() const { return mTrackedPosition; } const osg::Vec3d& getPosition() const { return mPosition; } void setStaticPosition(const osg::Vec3d& pos); bool isVanityOrPreviewModeEnabled() const { return mMode == Mode::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; } int getCollisionType() const { return mCollisionType; } void setCollisionType(int collisionType) { mCollisionType = collisionType; } 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; 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/cell.hpp000066400000000000000000000016241503074453300221460ustar00rootroot00000000000000#ifndef GAME_RENDER_CELL_H #define GAME_RENDER_CELL_H #include namespace MWRender { class CellRender { public: virtual ~CellRender() = default; /// 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; /// 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 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/characterpreview.cpp000066400000000000000000000623221503074453300245620ustar00rootroot00000000000000#include "characterpreview.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 "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/weapontype.hpp" #include "npcanimation.hpp" #include "util.hpp" #include "vismask.hpp" namespace MWRender { class DrawOnceCallback : public SceneUtil::NodeCallback { public: DrawOnceCallback(osg::Node* subgraph) : mRendered(false) , mLastRenderedFrame(0) , mSubgraph(subgraph) { } void operator()(osg::Node* node, osg::NodeVisitor* nv) { if (!mRendered) { mRendered = true; mLastRenderedFrame = nv->getTraversalNumber(); osg::ref_ptr previousFramestamp = const_cast(nv->getFrameStamp()); osg::FrameStamp* fs = new osg::FrameStamp(*previousFramestamp); fs->setSimulationTime(0.0); 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); } else { node->setNodeMask(0); } } void redrawNextFrame() { mRendered = false; } 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) { } void apply(osg::Node& node) override { if (osg::ref_ptr stateset = node.getStateSet()) { osg::ref_ptr newStateSet; if (stateset->getAttribute(osg::StateAttribute::BLENDFUNC) || stateset->getBinNumber() == osg::StateSet::TRANSPARENT_BIN) { osg::BlendFunc* blendFunc = static_cast(stateset->getAttribute(osg::StateAttribute::BLENDFUNC)); if (blendFunc) { newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); 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 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 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; } } if (stateset->getMode(GL_BLEND) & osg::StateAttribute::ON) { if (!newStateSet) { newStateSet = new osg::StateSet(*stateset, osg::CopyOp::SHALLOW_COPY); node.setStateSet(newStateSet); } // Disable noBlendAlphaEnv newStateSet->setTextureMode(7, GL_TEXTURE_2D, osg::StateAttribute::OFF); newStateSet->setDefine("FORCE_OPAQUE", "0", osg::StateAttribute::ON); } } traverse(node); } }; 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) : mParent(parent) , mResourceSystem(resourceSystem) , mPosition(position) , mLookAt(lookAt) , mCharacter(character) , mAnimation(nullptr) , mSizeX(sizeX) , mSizeY(sizeY) { 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); 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)); stateset->setAttribute(defaultMat); 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); fog->setStart(10000000); fog->setEnd(10000000); 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("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(); noBlendAlphaEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE); noBlendAlphaEnv->setSource0_Alpha(osg::TexEnvCombine::CONSTANT); noBlendAlphaEnv->setConstantColor(osg::Vec4(0.0, 0.0, 0.0, 1.0)); 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 dummyTexture->setShadowComparison(true); dummyTexture->setShadowCompareFunc(osg::Texture::ShadowCompareFunc::ALWAYS); stateset->setTextureAttributeAndModes(7, dummyTexture, osg::StateAttribute::ON); stateset->setTextureAttribute(7, noBlendAlphaEnv, osg::StateAttribute::ON); osg::ref_ptr lightmodel = new osg::LightModel; lightmodel->setAmbientIntensity(osg::Vec4(0.0, 0.0, 0.0, 1.0)); stateset->setAttributeAndModes(lightmodel, osg::StateAttribute::ON); osg::ref_ptr light = new osg::Light; float diffuseR = Fallback::Map::getFloat("Inventory_DirectionalDiffuseR"); float diffuseG = Fallback::Map::getFloat("Inventory_DirectionalDiffuseG"); float diffuseB = Fallback::Map::getFloat("Inventory_DirectionalDiffuseB"); float ambientR = Fallback::Map::getFloat("Inventory_DirectionalAmbientR"); float ambientG = Fallback::Map::getFloat("Inventory_DirectionalAmbientG"); float ambientB = Fallback::Map::getFloat("Inventory_DirectionalAmbientB"); float azimuth = osg::DegreesToRadians(Fallback::Map::getFloat("Inventory_DirectionalRotationX")); float altitude = osg::DegreesToRadians(Fallback::Map::getFloat("Inventory_DirectionalRotationY")); 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); 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)); } else light->setAmbient(ambientRGBA); light->setSpecular(osg::Vec4(0, 0, 0, 0)); light->setLightNum(0); light->setConstantAttenuation(1.f); light->setLinearAttenuation(0.f); light->setQuadraticAttenuation(0.f); lightManager->setSunlight(light); osg::ref_ptr lightSource = new osg::LightSource; lightSource->setLight(light); lightSource->setStateSetModes(*stateset, osg::StateAttribute::ON); lightManager->addChild(lightSource); mRTTNode->addChild(lightManager); mNode = new osg::PositionAttitudeTransform; lightManager->addChild(mNode); mDrawOnceCallback = new DrawOnceCallback(mRTTNode->mGroup); mRTTNode->addUpdateCallback(mDrawOnceCallback); mParent->addChild(mRTTNode); mCharacter.mCell = nullptr; } CharacterPreview::~CharacterPreview() { mParent->removeChild(mRTTNode); } int CharacterPreview::getTextureWidth() const { return mSizeX; } int CharacterPreview::getTextureHeight() const { return mSizeY; } void CharacterPreview::setBlendMode() { SetUpBlendVisitor visitor; mNode->accept(visitor); } void CharacterPreview::onSetup() { setBlendMode(); } osg::ref_ptr CharacterPreview::getTexture() { return static_cast(mRTTNode->getColorTexture(nullptr)); } void CharacterPreview::rebuild() { mAnimation = nullptr; mAnimation = new NpcAnimation(mCharacter, mNode, mResourceSystem, true, (renderHeadOnly() ? NpcAnimation::VM_HeadOnly : NpcAnimation::VM_Normal)); onSetup(); redraw(); } void CharacterPreview::redraw() { 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)) { } void InventoryPreview::setViewport(int sizeX, int sizeY) { sizeX = std::max(sizeX, 0); sizeY = std::max(sizeY, 0); // 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)); stateset->setAttributeAndModes(mViewport); mRTTNode->setCameraStateset(stateset); redraw(); } void InventoryPreview::update() { if (!mAnimation.get()) return; mAnimation->showWeapons(true); mAnimation->updateParts(); 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()) { groupname = "inventoryweapononehand"; if (iter->getType() == ESM::Weapon::sRecordId) { MWWorld::LiveCellRef* ref = iter->get(); int type = ref->mBase->mData.mType; const ESM::WeaponType* weaponInfo = MWMechanics::getWeaponType(type); showCarriedLeft = !(weaponInfo->mFlags & ESM::WeaponType::TwoHanded); std::string inventoryGroup = weaponInfo->mLongGroup; inventoryGroup = "inventory" + inventoryGroup; // We still should use one-handed animation as fallback if (mAnimation->hasAnimation(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; // 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) groupname = twoHandFallback; else groupname = oneHandFallback; } } } mAnimation->showCarriedLeft(showCarriedLeft); 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->getType() == ESM::Light::sRecordId && showCarriedLeft) { 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")) mAnimation->disable("torch"); mAnimation->runAnimation(0.0f); setBlendMode(); redraw(); } int InventoryPreview::getSlotSelected(int posX, int posY) { if (!mViewport) return -1; float projX = (posX / mViewport->width()) * 2 - 1.f; float projY = (posY / mViewport->height()) * 2 - 1.f; // With Intersector::WINDOW, the intersection ratios are slightly inaccurate. Seems to be a // 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)); 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 visitor.setTraversalNumber(mDrawOnceCallback->getLastRenderedFrame()); auto* camera = mRTTNode->getCamera(nullptr); osg::Node::NodeMask nodeMask = camera->getNodeMask(); camera->setNodeMask(~0u); camera->accept(visitor); camera->setNodeMask(nodeMask); if (intersector->containsIntersections()) { osgUtil::LineSegmentIntersector::Intersection intersection = intersector->getFirstIntersection(); return mAnimation->getSlot(intersection.nodePath); } return -1; } void InventoryPreview::updatePtr(const MWWorld::Ptr& ptr) { mCharacter = MWWorld::Ptr(ptr.getBase(), nullptr); } void InventoryPreview::onSetup() { CharacterPreview::onSetup(); osg::Vec3f scale(1.f, 1.f, 1.f); mCharacter.getClass().adjustScale(mCharacter, scale, true); mNode->setScale(scale); 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) , mRef(ESM::makeBlankCellRef(), &mBase) , mPitchRadians(osg::DegreesToRadians(6.f)) { mCharacter = MWWorld::Ptr(&mRef, nullptr); } 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))); redraw(); } void RaceSelectionPreview::setPrototype(const ESM::NPC& proto) { mBase = proto; mBase.mId = ESM::RefId::stringRefId("Player"); rebuild(); } class UpdateCameraCallback : public SceneUtil::NodeCallback { public: UpdateCameraCallback( osg::ref_ptr nodeToFollow, const osg::Vec3& posOffset, const osg::Vec3& lookAtOffset) : mNodeToFollow(std::move(nodeToFollow)) , mPosOffset(posOffset) , mLookAtOffset(lookAtOffset) { } void operator()(CharacterPreviewRTTNode* node, osg::NodeVisitor* nv) { // Update keyframe controllers in the scene graph first... traverse(node, nv); // Now update camera utilizing the updated head position osg::NodePathList nodepaths = mNodeToFollow->getParentalNodePaths(); if (nodepaths.empty()) return; osg::Matrix worldMat = osg::computeLocalToWorld(nodepaths[0]); osg::Vec3 headOffset = worldMat.getTrans(); auto viewMatrix = osg::Matrixf::lookAt(headOffset + mPosOffset, headOffset + mLookAtOffset, osg::Vec3(0, 0, 1)); node->setViewMatrix(viewMatrix); } private: osg::ref_ptr mNodeToFollow; osg::Vec3 mPosOffset; osg::Vec3 mLookAtOffset; }; void RaceSelectionPreview::onSetup() { CharacterPreview::onSetup(); 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) mRTTNode->removeUpdateCallback(mUpdateCameraCallback); const osg::Node* head = mAnimation->getNode("Bip01 Head"); if (head) { mUpdateCameraCallback = new UpdateCameraCallback(head, mPosition, mLookAt); mRTTNode->addUpdateCallback(mUpdateCameraCallback); } else Log(Debug::Error) << "Error: Bip01 Head node not found"; } } openmw-openmw-0.49.0/apps/openmw/mwrender/characterpreview.hpp000066400000000000000000000061441503074453300245670ustar00rootroot00000000000000#ifndef MWRENDER_CHARACTERPREVIEW_H #define MWRENDER_CHARACTERPREVIEW_H #include #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Texture2D; class Camera; class Group; class Viewport; class StateSet; } 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); virtual ~CharacterPreview(); int getTextureWidth() const; int getTextureHeight() const; void redraw(); 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&); CharacterPreview& operator=(const CharacterPreview&); protected: virtual bool renderHeadOnly() { return false; } void setBlendMode(); virtual void onSetup(); osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mTextureStateSet; osg::ref_ptr mDrawOnceCallback; osg::ref_ptr mRTTNode; osg::Vec3f mPosition; osg::Vec3f mLookAt; MWWorld::Ptr mCharacter; osg::ref_ptr mAnimation; osg::ref_ptr mNode; std::string mCurrentAnimGroup; int mSizeX; int mSizeY; }; class InventoryPreview : public CharacterPreview { public: InventoryPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem, const MWWorld::Ptr& character); void updatePtr(const MWWorld::Ptr& ptr); void update(); // Render preview again, e.g. after changed equipment void setViewport(int sizeX, int sizeY); int getSlotSelected(int posX, int posY); protected: osg::ref_ptr mViewport; void onSetup() override; }; class UpdateCameraCallback; class RaceSelectionPreview : public CharacterPreview { ESM::NPC mBase; MWWorld::LiveCellRef mRef; protected: bool renderHeadOnly() override { return true; } void onSetup() override; public: RaceSelectionPreview(osg::Group* parent, Resource::ResourceSystem* resourceSystem); virtual ~RaceSelectionPreview(); void setAngle(float angleRadians); const ESM::NPC& getPrototype() const { return mBase; } void setPrototype(const ESM::NPC& proto); private: osg::ref_ptr mUpdateCameraCallback; float mPitchRadians; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/creatureanimation.cpp000066400000000000000000000222221503074453300247310ustar00rootroot00000000000000#include "creatureanimation.hpp" #include #include #include #include #include #include #include #include #include "../mwmechanics/weapontype.hpp" #include "../mwworld/class.hpp" namespace MWRender { CreatureAnimation::CreatureAnimation( const MWWorld::Ptr& ptr, const std::string& model, Resource::ResourceSystem* resourceSystem, bool animated) : ActorAnimation(ptr, osg::ref_ptr(ptr.getRefData().getBaseNode()), resourceSystem) { MWWorld::LiveCellRef* ref = mPtr.get(); if (!model.empty()) { setObjectRoot(model, false, false, true); if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) addAnimSource(Settings::models().mXbaseanim.get(), model); if (animated) addAnimSource(model, model); } } 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) { MWWorld::LiveCellRef* ref = mPtr.get(); if (!model.empty()) { setObjectRoot(model, true, false, true); if ((ref->mBase->mFlags & ESM::Creature::Bipedal)) addAnimSource(Settings::models().mXbaseanim.get(), model); if (animated) addAnimSource(model, model); mPtr.getClass().getInventoryStore(mPtr).setInvListener(this); updateParts(); } mWeaponAnimationTime = std::make_shared(this); } void CreatureWeaponAnimation::showWeapons(bool showWeapon) { if (showWeapon != mShowWeapons) { mShowWeapons = showWeapon; updateParts(); } } void CreatureWeaponAnimation::showCarriedLeft(bool show) { if (show != mShowCarriedLeft) { mShowCarriedLeft = show; 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); if (it == inv.end()) { scene.reset(); return; } MWWorld::ConstPtr item = *it; std::string_view bonename; VFS::Path::Normalized itemModel = item.getClass().getCorrectedModel(item); if (slot == MWWorld::InventoryStore::Slot_CarriedRight) { if (item.getType() == ESM::Weapon::sRecordId) { int type = item.get()->mBase->mData.mType; bonename = MWMechanics::getWeaponType(type)->mAttachBone; if (bonename != "Weapon Bone") { const NodeMap& nodeMap = getNodeMap(); NodeMap::const_iterator found = nodeMap.find(bonename); if (found == nodeMap.end()) bonename = "Weapon Bone"; } } else bonename = "Weapon Bone"; } else { bonename = "Shield Bone"; if (item.getType() == ESM::Armor::sRecordId) { itemModel = getShieldMesh(item, false); } } try { osg::ref_ptr attached = attach(itemModel, bonename, bonename, item.getType() == ESM::Light::sRecordId); scene = std::make_unique(attached); 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.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(); std::shared_ptr source; if (slot == MWWorld::InventoryStore::Slot_CarriedRight) source = mWeaponAnimationTime; else source = mAnimationTimePtr[0]; 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(); } } bool CreatureWeaponAnimation::isArrowAttached() const { return mAmmunition != nullptr; } void CreatureWeaponAnimation::detachArrow() { WeaponAnimation::detachArrow(mPtr); updateQuiver(); } 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)); } updateQuiver(); } void CreatureWeaponAnimation::releaseArrow(float attackStrength) { WeaponAnimation::releaseArrow(mPtr, attackStrength); updateQuiver(); } osg::Group* CreatureWeaponAnimation::getArrowBone() { if (!mWeapon) 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; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; if (ammoType == ESM::Weapon::None) return nullptr; // 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; } osg::Node* CreatureWeaponAnimation::getWeaponNode() { return mWeapon ? mWeapon->getNode().get() : nullptr; } Resource::ResourceSystem* CreatureWeaponAnimation::getResourceSystem() { return mResourceSystem; } void CreatureWeaponAnimation::addControllers() { Animation::addControllers(); if (mObjectRoot) WeaponAnimation::addControllers(mNodeMap, mActiveControllers, mObjectRoot.get()); } osg::Vec3f CreatureWeaponAnimation::runAnimation(float duration) { osg::Vec3f ret = Animation::runAnimation(duration); WeaponAnimation::configureControllers(mPtr.getRefData().getPosition().rot[0] + getBodyPitchRadians()); return ret; } } openmw-openmw-0.49.0/apps/openmw/mwrender/creatureanimation.hpp000066400000000000000000000052301503074453300247360ustar00rootroot00000000000000#ifndef GAME_RENDER_CREATUREANIMATION_H #define GAME_RENDER_CREATUREANIMATION_H #include "../mwworld/inventorystore.hpp" #include "actoranimation.hpp" #include "weaponanimation.hpp" namespace MWWorld { class Ptr; } namespace MWRender { class CreatureAnimation : public ActorAnimation { public: 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 { public: 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; } void showCarriedLeft(bool show) override; void updateParts(); void updatePart(PartHolderPtr& scene, int slot); void attachArrow() override; void detachArrow() override; void releaseArrow(float attackStrength) override; // WeaponAnimation osg::Group* getArrowBone() override; 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 addControllers() override; osg::Vec3f runAnimation(float duration) 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; } protected: bool isArrowAttached() const override; private: PartHolderPtr mWeapon; PartHolderPtr mShield; bool mShowWeapons; bool mShowCarriedLeft; std::shared_ptr mWeaponAnimationTime; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/distortion.cpp000066400000000000000000000025701503074453300234210ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/distortion.hpp000066400000000000000000000014071503074453300234240ustar00rootroot00000000000000#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; }; } openmw-openmw-0.49.0/apps/openmw/mwrender/effectmanager.cpp000066400000000000000000000060221503074453300240060ustar00rootroot00000000000000#include "effectmanager.hpp" #include #include #include #include #include "animation.hpp" #include "util.hpp" #include "vismask.hpp" #include namespace MWRender { EffectManager::EffectManager(osg::ref_ptr parent, Resource::ResourceSystem* resourceSystem) : mParentNode(std::move(parent)) , mResourceSystem(resourceSystem) { } EffectManager::~EffectManager() { clear(); } void EffectManager::addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight) { osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); node->setNodeMask(Mask_Effect); Effect effect; effect.mAnimTime = std::make_shared(); 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); effect.mTransform = trans; SceneUtil::AssignControllerSourcesVisitor assignVisitor(effect.mAnimTime); node->accept(assignVisitor); if (isMagicVFX) overrideFirstRootTexture(textureOverride, mResourceSystem, *node); else overrideTexture(textureOverride, mResourceSystem, *node); mParentNode->addChild(trans); if (useAmbientLight) { // 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); mEffects.push_back(std::move(effect)); } 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 (const auto& effect : mEffects) { mParentNode->removeChild(effect.mTransform); } mEffects.clear(); } } openmw-openmw-0.49.0/apps/openmw/mwrender/effectmanager.hpp000066400000000000000000000030171503074453300240140ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_EFFECTMANAGER_H #define OPENMW_MWRENDER_EFFECTMANAGER_H #include #include #include #include namespace osg { class Group; class Vec3f; class PositionAttitudeTransform; } namespace Resource { class ResourceSystem; } namespace MWRender { class EffectAnimationTime; // Note: effects attached to another object should be managed by MWRender::Animation::addEffect. // This class manages "free" effects, i.e. attached to a dedicated scene node in the world. class EffectManager { 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(VFS::Path::NormalizedView model, std::string_view textureOverride, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true, bool useAmbientLight = true); void update(float dt); /// Remove all effects void clear(); private: struct Effect { float mMaxControllerLength; std::shared_ptr mAnimTime; osg::ref_ptr mTransform; }; std::vector mEffects; osg::ref_ptr mParentNode; Resource::ResourceSystem* mResourceSystem; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/esm4npcanimation.cpp000066400000000000000000000162121503074453300244720ustar00rootroot00000000000000#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( Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/esm4npcanimation.hpp000066400000000000000000000014001503074453300244700ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/fogmanager.cpp000066400000000000000000000065001503074453300233260ustar00rootroot00000000000000#include "fogmanager.hpp" #include #include #include #include #include #include #include #include "apps/openmw/mwworld/cell.hpp" namespace MWRender { FogManager::FogManager() : mLandFogStart(0.f) , mLandFogEnd(std::numeric_limits::max()) , mUnderwaterFogStart(0.f) , mUnderwaterFogEnd(std::numeric_limits::max()) , mFogColor(osg::Vec4f()) , mUnderwaterColor(Fallback::Map::getColour("Water_UnderwaterColor")) , mUnderwaterWeight(Fallback::Map::getFloat("Water_UnderwaterColorWeight")) , mUnderwaterIndoorFog(Fallback::Map::getFloat("Water_UnderwaterIndoorFog")) { } void FogManager::configure(float viewDistance, const MWWorld::Cell& cell) { osg::Vec4f color = SceneUtil::colourFromRGB(cell.getMood().mFogColor); const float fogDensity = cell.getMood().mFogDensity; if (Settings::fog().mUseDistantFog) { 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, fogDensity, mUnderwaterIndoorFog, 1.0f, 0.0f, color); } void FogManager::configure(float viewDistance, float fogDepth, float underwaterFog, float dlFactor, float dlOffset, const osg::Vec4f& color) { if (Settings::fog().mUseDistantFog) { 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 { if (fogDepth == 0.0) { mLandFogStart = 0.0f; mLandFogEnd = std::numeric_limits::max(); } else { mLandFogStart = viewDistance * (1 - fogDepth); mLandFogEnd = viewDistance; } mUnderwaterFogStart = std::min(viewDistance, 7168.f) * (1 - underwaterFog); mUnderwaterFogEnd = std::min(viewDistance, 7168.f); } mFogColor = color; } float FogManager::getFogStart(bool isUnderwater) const { return isUnderwater ? mUnderwaterFogStart : mLandFogStart; } float FogManager::getFogEnd(bool isUnderwater) const { return isUnderwater ? mUnderwaterFogEnd : mLandFogEnd; } osg::Vec4f FogManager::getFogColor(bool isUnderwater) const { if (isUnderwater) { return mUnderwaterColor * mUnderwaterWeight + mFogColor * (1.f - mUnderwaterWeight); } return mFogColor; } } openmw-openmw-0.49.0/apps/openmw/mwrender/fogmanager.hpp000066400000000000000000000015731503074453300233400ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_FOGMANAGER_H #define OPENMW_MWRENDER_FOGMANAGER_H #include namespace MWWorld { class Cell; } namespace MWRender { class FogManager { public: FogManager(); 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; float getFogEnd(bool isUnderwater) const; private: float mLandFogStart; float mLandFogEnd; float mUnderwaterFogStart; float mUnderwaterFogEnd; osg::Vec4f mFogColor; osg::Vec4f mUnderwaterColor; float mUnderwaterWeight; float mUnderwaterIndoorFog; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/globalmap.cpp000066400000000000000000000602071503074453300231620ustar00rootroot00000000000000#include "globalmap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "vismask.hpp" 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 geom = new osg::Geometry; osg::ref_ptr verts = new osg::Vec3Array; verts->push_back(osg::Vec3f(-1, -1, 0)); verts->push_back(osg::Vec3f(-1, 1, 0)); verts->push_back(osg::Vec3f(1, 1, 0)); verts->push_back(osg::Vec3f(1, -1, 0)); 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)); 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); geom->setTexCoordArray(0, texcoords, osg::Array::BIND_PER_VERTEX); geom->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::QUADS, 0, 4)); return geom; } class CameraUpdateGlobalCallback : public SceneUtil::NodeCallback { public: CameraUpdateGlobalCallback(MWRender::GlobalMap* parent) : mRendered(false) , mParent(parent) { } void operator()(osg::Camera* node, osg::NodeVisitor* nv) { if (mRendered) { if (mParent->copyResult(node, nv->getTraversalNumber())) { node->setNodeMask(0); mParent->markForRemoval(node); } return; } traverse(node, nv); mRendered = true; } private: bool mRendered; 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 { 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) { } void doWork() override { osg::ref_ptr image = new osg::Image; image->allocateImage(mWidth, mHeight, 1, GL_RGB, GL_UNSIGNED_BYTE); unsigned char* data = image->data(); osg::ref_ptr alphaImage = new osg::Image; alphaImage->allocateImage(mWidth, mHeight, 1, GL_ALPHA, GL_UNSIGNED_BYTE); unsigned char* alphaData = alphaImage->data(); for (int x = mMinX; x <= mMaxX; ++x) { for (int y = mMinY; y <= mMaxY; ++y) { const ESM::Land* land = mLandStore.search(x, y); for (int cellY = 0; cellY < mCellSize; ++cellY) { for (int cellX = 0; cellX < mCellSize; ++cellX) { int vertexX = static_cast(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; unsigned char r, g, b; float y2 = 0; if (land && (land->mDataTypes & ESM::Land::DATA_WNAM)) y2 = land->mWnam[vertexY * 9 + vertexX] / 128.f; else y2 = SCHAR_MIN / 128.f; if (y2 < 0) { r = static_cast(14 * y2 + 38); g = static_cast(20 * y2 + 56); b = static_cast(18 * y2 + 51); } else if (y2 < 0.3f) { if (y2 < 0.1f) y2 *= 8.f; else { y2 -= 0.1f; y2 += 0.8f; } r = static_cast(66 - 32 * y2); g = static_cast(48 - 23 * y2); b = static_cast(33 - 16 * y2); } else { y2 -= 0.3f; y2 *= 1.428f; r = static_cast(34 - 29 * y2); g = static_cast(25 - 20 * y2); b = static_cast(17 - 12 * y2); } data[texelY * mWidth * 3 + texelX * 3] = r; 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); } } } } mBaseTexture = new osg::Texture2D; mBaseTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mBaseTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mBaseTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mBaseTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mBaseTexture->setImage(image); mBaseTexture->setResizeNonPowerOfTwoHint(false); mAlphaTexture = new osg::Texture2D; mAlphaTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mAlphaTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mAlphaTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mAlphaTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mAlphaTexture->setImage(alphaImage); mAlphaTexture->setResizeNonPowerOfTwoHint(false); mOverlayImage = new osg::Image; mOverlayImage->allocateImage(mWidth, mHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE); assert(mOverlayImage->isDataContiguous()); memset(mOverlayImage->data(), 0, mOverlayImage->getTotalSizeInBytes()); mOverlayTexture = new osg::Texture2D; mOverlayTexture->setWrap(osg::Texture::WRAP_S, osg::Texture::CLAMP_TO_EDGE); mOverlayTexture->setWrap(osg::Texture::WRAP_T, osg::Texture::CLAMP_TO_EDGE); mOverlayTexture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); mOverlayTexture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); mOverlayTexture->setResizeNonPowerOfTwoHint(false); mOverlayTexture->setInternalFormat(GL_RGBA); mOverlayTexture->setTextureSize(mWidth, mHeight); } int mWidth, mHeight; int mMinX, mMinY, mMaxX, mMaxY; int mCellSize; const MWWorld::Store& mLandStore; osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; osg::ref_ptr mOverlayImage; 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) { } GlobalMap::~GlobalMap() { for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); for (auto& camera : mActiveCameras) removeCamera(camera); if (mWorkItem) mWorkItem->waitTillDone(); } void GlobalMap::render() { const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); // get the size of the world MWWorld::Store::iterator it = esmStore.get().extBegin(); for (; it != esmStore.get().extEnd(); ++it) { if (it->getGridX() < mMinX) mMinX = it->getGridX(); if (it->getGridX() > mMaxX) mMaxX = it->getGridX(); if (it->getGridY() < mMinY) mMinY = it->getGridY(); if (it->getGridY() > mMaxY) mMaxY = it->getGridY(); } 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, 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)) * getWidth(); imageY = (1.f - float(z / float(Constants::CellSizeInUnits) - mMinY) / (mMaxY - mMinY + 1)) * getHeight(); } void GlobalMap::requestOverlayTextureUpdate(int x, int y, int width, int height, osg::ref_ptr texture, bool clear, bool cpuCopy, float srcLeft, float srcTop, float srcRight, float srcBottom) { osg::ref_ptr camera(new osg::Camera); camera->setNodeMask(Mask_RenderToTexture); camera->setReferenceFrame(osg::Camera::ABSOLUTE_RF); camera->setViewMatrix(osg::Matrix::identity()); camera->setProjectionMatrix(osg::Matrix::identity()); camera->setProjectionResizePolicy(osg::Camera::FIXED); camera->setRenderOrder(osg::Camera::PRE_RENDER, 1); // Make sure the global map is rendered after the local map y = mHeight - y - height; // convert top-left origin to bottom-left camera->setViewport(x, y, width, height); if (clear) { camera->setClearMask(GL_COLOR_BUFFER_BIT); camera->setClearColor(osg::Vec4(0, 0, 0, 0)); } else camera->setClearMask(GL_NONE); camera->setUpdateCallback(new CameraUpdateGlobalCallback(this)); camera->setRenderTargetImplementation(osg::Camera::FRAME_BUFFER_OBJECT, osg::Camera::PIXEL_BUFFER_RTT); camera->attach(osg::Camera::COLOR_BUFFER, mOverlayTexture); // no need for a depth buffer camera->setImplicitBufferAttachmentMask(osg::DisplaySettings::IMPLICIT_COLOR_BUFFER_ATTACHMENT); if (cpuCopy) { // Attach an image to copy the render back to the CPU when finished osg::ref_ptr image(new osg::Image); image->setPixelFormat(mOverlayImage->getPixelFormat()); image->setDataType(mOverlayImage->getDataType()); camera->attach(osg::Camera::COLOR_BUFFER, image); ImageDest imageDest; imageDest.mImage = image; imageDest.mX = x; imageDest.mY = y; 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 SceneUtil::AutoDepth; depth->setWriteMask(false); osg::StateSet* stateset = geom->getOrCreateStateSet(); stateset->setAttribute(depth); stateset->setTextureAttributeAndModes(0, texture, osg::StateAttribute::ON); stateset->setMode(GL_LIGHTING, osg::StateAttribute::OFF); stateset->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF); if (mAlphaTexture) { osg::ref_ptr texcoords = new osg::Vec2Array; float x1 = x / static_cast(mWidth); float x2 = (x + width) / static_cast(mWidth); float y1 = y / static_cast(mHeight); float y2 = (y + height) / static_cast(mHeight); texcoords->push_back(osg::Vec2f(x1, y1)); texcoords->push_back(osg::Vec2f(x1, y2)); texcoords->push_back(osg::Vec2f(x2, y2)); texcoords->push_back(osg::Vec2f(x2, y1)); geom->setTexCoordArray(1, texcoords, osg::Array::BIND_PER_VERTEX); stateset->setTextureAttributeAndModes(1, mAlphaTexture, osg::StateAttribute::ON); osg::ref_ptr texEnvCombine = new osg::TexEnvCombine; texEnvCombine->setCombine_RGB(osg::TexEnvCombine::REPLACE); texEnvCombine->setSource0_RGB(osg::TexEnvCombine::PREVIOUS); stateset->setTextureAttributeAndModes(1, texEnvCombine); } camera->addChild(geom); } mRoot->addChild(camera); mActiveCameras.push_back(camera); } void GlobalMap::exploreCell(int cellX, int cellY, osg::ref_ptr localMapTexture) { ensureLoaded(); if (!localMapTexture) return; 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, cellSize, cellSize, std::move(localMapTexture), false, true); } void GlobalMap::clear() { ensureLoaded(); memset(mOverlayImage->data(), 0, mOverlayImage->getTotalSizeInBytes()); mPendingImageDest.clear(); // just push a Camera to clear the FBO, instead of setImage()/dirty() // easier, since we don't need to worry about synchronizing access :) requestOverlayTextureUpdate(0, 0, mWidth, mHeight, osg::ref_ptr(), true, false); } void GlobalMap::write(ESM::GlobalMap& map) { ensureLoaded(); map.mBounds.mMinX = mMinX; map.mBounds.mMaxX = mMaxX; map.mBounds.mMinY = mMinY; map.mBounds.mMaxY = mMaxY; if (mWritePng != nullptr) { mWritePng->waitTillDone(); map.mImageData = std::move(mWritePng->mImageData); mWritePng = nullptr; return; } map.mImageData = writePng(*mOverlayImage); } struct Box { int mLeft, mTop, mRight, mBottom; Box(int left, int top, int right, int bottom) : mLeft(left) , mTop(top) , mRight(right) , mBottom(bottom) { } bool operator==(const Box& other) const { return mLeft == other.mLeft && mTop == other.mTop && mRight == other.mRight && mBottom == other.mBottom; } }; void GlobalMap::read(ESM::GlobalMap& map) { ensureLoaded(); const ESM::GlobalMap::Bounds& bounds = map.mBounds; if (bounds.mMaxX - bounds.mMinX < 0) return; if (bounds.mMaxY - bounds.mMinY < 0) return; if (bounds.mMinX > bounds.mMaxX || bounds.mMinY > bounds.mMaxY) throw std::runtime_error("invalid map bounds"); if (map.mImageData.empty()) return; Files::IMemStream istream(map.mImageData.data(), map.mImageData.size()); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("png"); if (!readerwriter) { Log(Debug::Error) << "Error: Can't read map overlay: no png readerwriter found"; return; } osgDB::ReaderWriter::ReadResult result = readerwriter->readImage(istream); if (!result.success()) { Log(Debug::Error) << "Error: Can't read map overlay: " << result.message() << " code " << result.status(); return; } osg::ref_ptr image = result.getImage(); int imageWidth = image->s(); int imageHeight = image->t(); 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; if (int(imageHeight / yLength) != cellImageSizeSrc) throw std::runtime_error("cell size must be quadratic"); // 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 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) return; int leftDiff = (mMinX - bounds.mMinX); int topDiff = (bounds.mMaxY - mMaxY); int rightDiff = (bounds.mMaxX - mMaxX); 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 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); 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); texture->setFilter(osg::Texture::MIN_FILTER, osg::Texture::LINEAR); texture->setFilter(osg::Texture::MAG_FILTER, osg::Texture::LINEAR); texture->setResizeNonPowerOfTwoHint(false); if (srcBox == destBox && imageWidth == mWidth && imageHeight == mHeight) { mOverlayImage = image; 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, std::move(texture), true, true, srcBox.mLeft / float(imageWidth), srcBox.mTop / float(imageHeight), srcBox.mRight / float(imageWidth), srcBox.mBottom / float(imageHeight)); } } osg::ref_ptr GlobalMap::getBaseTexture() { ensureLoaded(); return mBaseTexture; } osg::ref_ptr GlobalMap::getOverlayTexture() { ensureLoaded(); return mOverlayTexture; } void GlobalMap::ensureLoaded() { if (mWorkItem) { mWorkItem->waitTillDone(); mOverlayImage = mWorkItem->mOverlayImage; mBaseTexture = mWorkItem->mBaseTexture; mAlphaTexture = mWorkItem->mAlphaTexture; mOverlayTexture = mWorkItem->mOverlayTexture; requestOverlayTextureUpdate(0, 0, mWidth, mHeight, osg::ref_ptr(), true, false); mWorkItem = nullptr; } } bool GlobalMap::copyResult(osg::Camera* camera, unsigned int frame) { ImageDestMap::iterator it = mPendingImageDest.find(camera); if (it == mPendingImageDest.end()) return true; 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 > frame) { ++it; return false; } mOverlayImage->copySubImage(imageDest.mX, imageDest.mY, 0, imageDest.mImage); mPendingImageDest.erase(it); return true; } } void GlobalMap::markForRemoval(osg::Camera* camera) { CameraVector::iterator found = std::find(mActiveCameras.begin(), mActiveCameras.end(), camera); if (found == mActiveCameras.end()) { Log(Debug::Error) << "Error: GlobalMap trying to remove an inactive camera"; return; } mActiveCameras.erase(found); mCamerasPendingRemoval.push_back(camera); } void GlobalMap::cleanupCameras() { for (auto& camera : mCamerasPendingRemoval) removeCamera(camera); mCamerasPendingRemoval.clear(); } 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/globalmap.hpp000066400000000000000000000066741503074453300231770ustar00rootroot00000000000000#ifndef GAME_RENDER_GLOBALMAP_H #define GAME_RENDER_GLOBALMAP_H #include #include #include #include namespace osg { class Texture2D; class Image; class Group; class Camera; } namespace ESM { struct GlobalMap; } namespace SceneUtil { class WorkQueue; } namespace MWRender { class CreateMapWorkItem; class GlobalMap { public: GlobalMap(osg::Group* root, SceneUtil::WorkQueue* workQueue); ~GlobalMap(); void render(); int getWidth() const { return mWidth; } int getHeight() const { return mHeight; } void worldPosToImageSpace(float x, float z, float& imageX, float& imageY); void exploreCell(int cellX, int cellY, osg::ref_ptr localMapTexture); /// Clears the overlay void clear(); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that * we do not render the same map more than once. Note, this cleanup is difficult to implement in an * automated fashion, since we can't alter the scene graph structure from within an update callback. */ void cleanupCameras(); void removeCamera(osg::Camera* cam); bool copyResult(osg::Camera* cam, unsigned int frame); /** * Mark a camera for cleanup in the next update. For internal use only. */ void markForRemoval(osg::Camera* camera); 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); osg::ref_ptr mRoot; typedef std::vector> CameraVector; CameraVector mActiveCameras; CameraVector mCamerasPendingRemoval; struct ImageDest { ImageDest() : mX(0) , mY(0) , mFrameDone(0) { } osg::ref_ptr mImage; int mX, mY; unsigned int mFrameDone; }; typedef std::map, ImageDest> ImageDestMap; ImageDestMap mPendingImageDest; osg::ref_ptr mBaseTexture; osg::ref_ptr mAlphaTexture; // GPU copy of overlay // Note, uploads are pushed through a Camera, instead of through mOverlayImage osg::ref_ptr mOverlayTexture; // CPU copy of overlay osg::ref_ptr mOverlayImage; osg::ref_ptr mWorkQueue; osg::ref_ptr mWorkItem; osg::ref_ptr mWritePng; int mWidth; int mHeight; int mMinX, mMaxX, mMinY, mMaxY; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/groundcover.cpp000066400000000000000000000516721503074453300235670ustar00rootroot00000000000000#include "groundcover.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/groundcoverstore.hpp" #include "vismask.hpp" namespace MWRender { namespace { using value_type = osgUtil::CullVisitor::value_type; // From OSG's CullVisitor.cpp inline value_type distance(const osg::Vec3& coord, const osg::Matrix& matrix) { 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)); } 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); } class InstancedComputeNearFarCullCallback : public osg::DrawableCullCallback { public: explicit InstancedComputeNearFarCullCallback(std::span instances, const osg::Vec3& chunkPosition, const osg::BoundingBox& instanceBounds) : mInstanceMatrices() , mInstanceBounds(instanceBounds) { mInstanceMatrices.reserve(instances.size()); for (const Groundcover::GroundcoverEntry& instance : instances) mInstanceMatrices.emplace_back(computeInstanceMatrix(instance, chunkPosition)); } 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(); if (cnfMode != osg::CullSettings::COMPUTE_NEAR_FAR_USING_PRIMITIVES && cnfMode != osg::CullSettings::COMPUTE_NEAR_USING_PRIMITIVES) return false; if (drawable->isCullingActive() && cullVisitor.isCulled(boundingBox)) return true; 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); if (dNear > dFar) std::swap(dNear, dFar); 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; } private: std::vector mInstanceMatrices; osg::BoundingBox mInstanceBounds; }; class InstancingVisitor : public osg::NodeVisitor { public: explicit InstancingVisitor( std::span instances, osg::Vec3f& chunkPosition) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mInstances(instances) , mChunkPosition(chunkPosition) { } void apply(osg::Group& group) override { for (unsigned int i = 0; i < group.getNumChildren();) { if (group.getChild(i)->asDrawable() && !group.getChild(i)->asGeometry()) group.removeChild(i); else ++i; } traverse(group); } void apply(osg::Geometry& geom) override { for (unsigned int i = 0; i < geom.getNumPrimitiveSets(); ++i) { geom.getPrimitiveSet(i)->setNumInstances(mInstances.size()); } 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.setInitialBound(box); osg::ref_ptr rotations = new osg::Vec3Array(mInstances.size()); for (unsigned int i = 0; i < rotations->getNumElements(); i++) { (*rotations)[i] = mInstances[i].mPos.asRotationVec3(); } // Display lists do not support instancing in OSG 3.4 geom.setUseDisplayList(false); geom.setUseVertexBufferObjects(true); geom.setVertexAttribArray(6, transforms.get(), osg::Array::BIND_PER_VERTEX); geom.setVertexAttribArray(7, rotations.get(), osg::Array::BIND_PER_VERTEX); geom.addCullCallback(new InstancedComputeNearFarCullCallback(mInstances, mChunkPosition, originalBox)); } private: std::span mInstances; osg::Vec3f mChunkPosition; }; class DensityCalculator { public: DensityCalculator(float density) : mDensity(density) { } bool isInstanceEnabled() { if (mDensity >= 1.f) return true; mCurrentGroundcover += mDensity; if (mCurrentGroundcover < 1.f) return false; mCurrentGroundcover -= 1.f; return true; } void reset() { mCurrentGroundcover = 0.f; } 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); } 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; } } osg::ref_ptr Groundcover::getChunk(float size, const osg::Vec2f& center, unsigned char lod, unsigned int lodFlags, bool activeGrid, const osg::Vec3f& viewPoint, bool compile) { if (lod > getMaxLodLevel()) return nullptr; GroundcoverChunkId id = std::make_tuple(center, size); osg::ref_ptr obj = mCache->getRefFromObjectCache(id); if (obj) return static_cast(obj.get()); else { InstanceMap instances; collectInstances(instances, size, center); osg::ref_ptr node = createChunk(instances, center); mCache->addEntryToObjectCache(id, node.get()); return node; } } 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) { 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); 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) { ESM::Cell cell; mGroundcoverStore.initCell(cell, cellX, cellY); if (cell.mContextList.empty()) continue; calculator.reset(); std::map refs; for (size_t i = 0; i < cell.mContextList.size(); ++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; bool deleted = false; while (cell.getNextRef(*reader, ref, deleted)) { 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& [refNum, cellRef] : refs) { const VFS::Path::NormalizedView model = mGroundcoverStore.getGroundcoverModel(cellRef.mRefID); if (model.empty()) continue; auto it = instances.find(model); if (it == instances.end()) it = instances.emplace_hint(it, VFS::Path::Normalized(model), std::vector()); it->second.emplace_back(std::move(cellRef)); } } } } 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; for (const auto& [model, entries] : instances) { const osg::Node* temp = mSceneManager->getTemplate(model); 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)); InstancingVisitor visitor(entries, worldCenter); node->accept(visitor); group->addChild(node); } osg::ComputeBoundsVisitor cbv; group->accept(cbv); osg::BoundingBox box = cbv.getBoundingBox(); group->addCullCallback(new ViewDistanceCallback(getViewDistance(), box)); group->setStateSet(mStateset); group->setNodeMask(Mask_Groundcover); if (mSceneManager->getLightingMethod() != SceneUtil::LightingMethod::FFP) group->addCullCallback(new SceneUtil::LightListCallback); mSceneManager->recreateShaders(group, "groundcover", true, mProgramTemplate); mSceneManager->shareState(group); group->getBound(); return group; } unsigned int Groundcover::getNodeMask() { return Mask_Groundcover; } void Groundcover::reportStats(unsigned int frameNumber, osg::Stats* stats) const { Resource::reportStats("Groundcover Chunk", frameNumber, mCache->getStats(), *stats); } } openmw-openmw-0.49.0/apps/openmw/mwrender/groundcover.hpp000066400000000000000000000036501503074453300235650ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_GROUNDCOVER_H #define OPENMW_MWRENDER_GROUNDCOVER_H #include #include #include #include namespace MWWorld { class ESMStore; class GroundcoverStore; } namespace osg { class Program; } namespace MWRender { typedef std::tuple GroundcoverChunkId; // Center, Size class Groundcover : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: 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; unsigned int getNodeMask() override; void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; struct GroundcoverEntry { ESM::Position mPos; float mScale; GroundcoverEntry(const ESM::CellRef& ref) : mPos(ref.mPos) , mScale(ref.mScale) { } }; private: using InstanceMap = std::map, std::less<>>; Resource::SceneManager* mSceneManager; float mDensity; osg::ref_ptr mStateset; osg::ref_ptr mProgramTemplate; const MWWorld::GroundcoverStore& mGroundcoverStore; osg::ref_ptr createChunk(InstanceMap& instances, const osg::Vec2f& center); void collectInstances(InstanceMap& instances, float size, const osg::Vec2f& center); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/landmanager.cpp000066400000000000000000000040061503074453300234700ustar00rootroot00000000000000#include "landmanager.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWRender { LandManager::LandManager(int loadFlags) : GenericResourceManager(nullptr, Settings::cells().mCacheExpiryDelay) , mLoadFlags(loadFlags) { } osg::ref_ptr LandManager::getLand(ESM::ExteriorCellLocation cellIndex) { 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 { Resource::reportStats("Land", frameNumber, mCache->getStats(), *stats); } } openmw-openmw-0.49.0/apps/openmw/mwrender/landmanager.hpp000066400000000000000000000013421503074453300234750ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_LANDMANAGER_H #define OPENMW_MWRENDER_LANDMANAGER_H #include #include #include #include namespace ESM { struct Land; } namespace MWRender { class LandManager : public Resource::GenericResourceManager { public: LandManager(int loadFlags); /// @note Will return nullptr if not found. osg::ref_ptr getLand(ESM::ExteriorCellLocation cellIndex); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; private: int mLoadFlags; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/localmap.cpp000066400000000000000000000735641503074453300230260ustar00rootroot00000000000000#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 "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwworld/cellstore.hpp" #include "util.hpp" #include "vismask.hpp" namespace { float square(float val) { return val * val; } std::pair divideIntoSegments(const osg::BoundingBox& bounds, float mapSize) { osg::Vec2f min(bounds.xMin(), bounds.yMin()); osg::Vec2f max(bounds.xMax(), bounds.yMax()); 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 }; } } 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); void setDefaults(osg::Camera* camera) override; osg::Node* mSceneRoot; osg::Matrix mProjectionMatrix; osg::Matrix mViewMatrix; bool mActive; }; class CameraLocalUpdateCallback : public SceneUtil::NodeCallback { public: void operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv); }; 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"); } 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) { 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(); } void LocalMap::saveFogOfWar(MWWorld::CellStore* cell) { if (!mInterior) { 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()); cell->setFog(std::move(fog)); } } else { auto segments = divideIntoSegments(mBounds, mMapWorldSize); 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->mCenterX = mCenter.x(); fog->mCenterY = mCenter.y(); fog->mFogTextures.reserve(segments.first * segments.second); for (int x = 0; x < segments.first; ++x) { for (int y = 0; y < segments.second; ++y) { const MapSegment& segment = mInteriorSegments[std::make_pair(x, y)]; if (!segment.mHasFogState) continue; ESM::FogTexture& texture = fog->mFogTextures.emplace_back(); segment.saveFogOfWar(texture); texture.mX = x; texture.mY = y; } } cell->setFog(std::move(fog)); } } 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)); mRoot->addChild(mLocalMapRTTs.back()); 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) { if (!cell->isExterior()) { requestInteriorMap(cell); return; } int cellX = cell->getCell()->getGridX(); int cellY = cell->getCell()->getGridY(); MapSegment& segment = mExteriorSegments[std::make_pair(cellX, cellY)]; const std::uint8_t neighbourFlags = getExteriorNeighbourFlags(cellX, cellY); if ((segment.mLastRenderNeighbourFlags & neighbourFlags) == neighbourFlags) return; requestExteriorMap(cell, segment); segment.mLastRenderNeighbourFlags = neighbourFlags; } void LocalMap::addCell(MWWorld::CellStore* cell) { if (cell->isExterior()) mExteriorSegments.emplace( std::make_pair(cell->getCell()->getGridX(), cell->getCell()->getGridY()), MapSegment{}); } void LocalMap::removeExteriorCell(int x, int y) { mExteriorSegments.erase({ x, y }); } void LocalMap::removeCell(MWWorld::CellStore* cell) { saveFogOfWar(cell); if (cell->isExterior()) mExteriorSegments.erase({ cell->getCell()->getGridX(), cell->getCell()->getGridY() }); else mInteriorSegments.clear(); } osg::ref_ptr LocalMap::getMapTexture(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.mMapTexture; } 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; } void LocalMap::cleanupCameras() { auto it = mLocalMapRTTs.begin(); while (it != mLocalMapRTTs.end()) { if (!(*it)->mActive) { mRoot->removeChild(*it); it = mLocalMapRTTs.erase(it); } else it++; } } void LocalMap::requestExteriorMap(const MWWorld::CellStore* cell, MapSegment& segment) { mInterior = false; const int x = cell->getCell()->getGridX(); const int y = cell->getCell()->getGridY(); 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); if (segment.mFogOfWarImage != nullptr) return; if (cell->getFog() && !cell->getFog()->mFogTextures.empty()) segment.loadFogOfWar(cell->getFog()->mFogTextures.back()); else segment.initFogOfWar(); } static osg::Vec2f getNorthVector(const MWWorld::CellStore* cell) { MWWorld::ConstPtr northmarker = cell->searchConst(ESM::RefId::stringRefId("northmarker")); if (northmarker.isEmpty()) return osg::Vec2f(0, 1); 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; } void LocalMap::requestInteriorMap(const MWWorld::CellStore* cell) { osg::ComputeBoundsVisitor computeBoundsVisitor; computeBoundsVisitor.setTraversalMask(Mask_Scene | Mask_Terrain | Mask_Object | Mask_Static); mSceneRoot->accept(computeBoundsVisitor); osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); // 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; mInterior = true; mBounds = bounds; // Get the cell's NorthMarker rotation. This is used to rotate the entire map. osg::Vec2f north = getNorthVector(cell); mAngle = std::atan2(north.x(), north.y()); // 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); 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)); } // 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)); float zMin = mBounds.zMin(); float zMax = mBounds.zMax(); mCenter = osg::Vec2f(mBounds.center().x(), mBounds.center().y()); // 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. int xOffset = 0; int yOffset = 0; if (const ESM::FogState* fog = cell->getFog()) { if (std::abs(mAngle - fog->mNorthMarkerAngle) < osg::DegreesToRadians(5.f)) { // Expand mBounds so the saved textures fit the same grid 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 = std::ceil(diff / mMapWorldSize); 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 = std::ceil(diff / mMapWorldSize); 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; mAngle = fog->mNorthMarkerAngle; mCenter.x() = fog->mCenterX; mCenter.y() = fog->mCenterY; } } osg::Vec2f min(mBounds.xMin(), mBounds.yMin()); 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) { 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 - mCenter; osg::Vec3f rotatedCenter = cameraOrient * (osg::Vec3f(a.x(), a.y(), 0)); osg::Vec2f pos = osg::Vec2f(rotatedCenter.x(), rotatedCenter.y()) + mCenter; 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 = mInteriorSegments[coords]; if (!segment.mFogOfWarImage) { bool loaded = false; if (const ESM::FogState* fog = cell->getFog()) { auto match = std::find_if( fog->mFogTextures.begin(), fog->mFogTextures.end(), [&](const ESM::FogTexture& texture) { return texture.mX == x - xOffset && texture.mY == y - yOffset; }); if (match != fog->mFogTextures.end()) { segment.loadFogOfWar(*match); loaded = true; } } if (!loaded) segment.initFogOfWar(); } } } } void LocalMap::worldToInteriorMapPosition(osg::Vec2f pos, float& nX, float& nY, int& x, int& y) { pos = rotatePoint(pos, mCenter, 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); nX = (pos.x() - min.x() - mMapWorldSize * x) / mMapWorldSize; nY = 1.0f - (pos.y() - min.y() - mMapWorldSize * y) / mMapWorldSize; } 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()); pos = rotatePoint(pos, mCenter, -mAngle); return pos; } 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; nX = std::clamp(nX, 0.f, 1.f); nY = std::clamp(nY, 0.f, 1.f); int texU = static_cast((sFogOfWarResolution - 1) * nX); int texV = static_cast((sFogOfWarResolution - 1) * nY); const std::uint32_t clr = reinterpret_cast(segment.mFogOfWarImage->data())[texV * sFogOfWarResolution + texU]; uint8_t alpha = (clr >> 24); return alpha < 200; } osg::Group* LocalMap::getRoot() { return mRoot; } 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()); if (mInterior) { worldToInteriorMapPosition(pos, u, v, x, y); 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); x = static_cast(std::ceil(pos.x() / mMapWorldSize) - 1); y = static_cast(std::ceil(pos.y() / mMapWorldSize) - 1); // 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) { for (int my = -mCellDistance; my <= mCellDistance; ++my) { // 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; int texX = x + mx; int texY = y + my * -1; auto& segments(mInterior ? mInteriorSegments : mExteriorSegments); MapSegment& segment = segments[std::make_pair(texX, texY)]; if (!segment.mFogOfWarImage || !segment.mMapTexture) continue; std::uint32_t* data = reinterpret_cast(segment.mFogOfWarImage->data()); bool changed = false; for (int texV = 0; texV < sFogOfWarResolution; ++texV) { for (int texU = 0; texU < sFogOfWarResolution; ++texU) { 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; } } if (changed) { segment.mHasFogState = true; segment.mFogOfWarImage->dirty(); } } } } 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; } MyGUI::IntRect LocalMap::getInteriorGrid() const { auto segments = divideIntoSegments(mBounds, mMapWorldSize); return { -1, -1, segments.first, segments.second }; } 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); memcpy(mFogOfWarImage->data(), data.data(), data.size() * 4); createFogOfWarTexture(); } void LocalMap::MapSegment::loadFogOfWar(const ESM::FogTexture& esm) { 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; } void LocalMap::MapSegment::saveFogOfWar(ESM::FogTexture& fog) const { if (!mFogOfWarImage) return; std::ostringstream ostream; 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()); } 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); 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); mViewMatrix.makeLookAt(osg::Vec3d(x, y, zmax + 5), osg::Vec3d(x, y, zmin), upVector); setUpdateCallback(new CameraLocalUpdateCallback); setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); } void LocalMapRenderToTexture::setDefaults(osg::Camera* camera) { // 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("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); } void CameraLocalUpdateCallback::operator()(LocalMapRenderToTexture* node, osg::NodeVisitor* nv) { if (!node->mActive) node->setNodeMask(0); 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/localmap.hpp000066400000000000000000000117761503074453300230300ustar00rootroot00000000000000#ifndef GAME_RENDER_LOCALMAP_H #define GAME_RENDER_LOCALMAP_H #include #include #include #include #include #include #include #include namespace MWWorld { class CellStore; } namespace ESM { struct FogTexture; } namespace osg { class Texture2D; class Image; class Camera; class Group; class Node; } namespace MWRender { class LocalMapRenderToTexture; /// /// \brief Local map rendering /// class LocalMap { public: LocalMap(osg::Group* root); ~LocalMap(); /** * Clear all savegame-specific data (i.e. fog of war textures) */ void clear(); /** * 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 addCell(MWWorld::CellStore* cell); void removeExteriorCell(int x, int y); void removeCell(MWWorld::CellStore* cell); osg::ref_ptr getMapTexture(int x, int y); osg::ref_ptr getFogOfWarTexture(int x, int y); /** * Removes cameras that have already been rendered. Should be called every frame to ensure that * we do not render the same map more than once. Note, this cleanup is difficult to implement in an * automated fashion, since we can't alter the scene graph structure from within an update callback. */ void cleanupCameras(); /** * 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); /** * Save the fog of war for this cell to its CellStore. * @remarks This should be called when unloading a cell, and for all active cells prior to saving the game. */ void saveFogOfWar(MWWorld::CellStore* cell); /** * 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); 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); osg::Group* getRoot(); MyGUI::IntRect getInteriorGrid() const; private: osg::ref_ptr mRoot; osg::ref_ptr mSceneRoot; typedef std::vector> RTTVector; RTTVector mLocalMapRTTs; enum NeighbourCellFlag : std::uint8_t { 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; }; typedef std::map, MapSegment> SegmentMap; SegmentMap mExteriorSegments; SegmentMap mInteriorSegments; int mMapResolution; // the dynamic texture is a bottleneck, so don't set this too high static const int sFogOfWarResolution = 32; // size of a map segment (for exteriors, 1 cell) float mMapWorldSize; int mCellDistance; float mAngle; const osg::Vec2f rotatePoint(const osg::Vec2f& point, const osg::Vec2f& center, const float angle); void requestExteriorMap(const MWWorld::CellStore* cell, MapSegment& segment); void requestInteriorMap(const MWWorld::CellStore* cell); void setupRenderToTexture( int segment_x, int segment_y, float left, float top, const osg::Vec3d& upVector, float zmin, float zmax); osg::BoundingBox mBounds; osg::Vec2f mCenter; bool mInterior; std::uint8_t getExteriorNeighbourFlags(int cellX, int cellY) const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/luminancecalculator.cpp000066400000000000000000000161401503074453300252460ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/openmw/mwrender/luminancecalculator.hpp000066400000000000000000000034631503074453300252570ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/navmesh.cpp000066400000000000000000000274311503074453300226670ustar00rootroot00000000000000#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 { 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) , mMode(mode) , mId(std::numeric_limits::max()) { } NavMesh::~NavMesh() { if (mEnabled) disable(); for (const auto& workItem : mWorkItems) workItem->abort(); } bool NavMesh::toggle() { if (mEnabled) disable(); else enable(); return mEnabled; } void NavMesh::update(const std::shared_ptr& navMesh, std::size_t id, const DetourNavigator::Settings& settings) { 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; if (mId != id) { 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() { for (auto& workItem : mWorkItems) workItem->abort(); mWorkItems.clear(); for (auto& [position, tile] : mTiles) mRootNode->removeChild(tile.mGroup); mTiles.clear(); } void NavMesh::enable() { mEnabled = true; } void NavMesh::disable() { reset(); mEnabled = false; } void NavMesh::setMode(Settings::NavMeshRenderMode value) { if (mMode == value) return; reset(); mMode = value; } } openmw-openmw-0.49.0/apps/openmw/mwrender/navmesh.hpp000066400000000000000000000037041503074453300226710ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_NAVMESH_H #define OPENMW_MWRENDER_NAVMESH_H #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 { class NavMesh { public: explicit NavMesh(const osg::ref_ptr& root, const osg::ref_ptr& workQueue, bool enabled, Settings::NavMeshRenderMode mode); ~NavMesh(); bool toggle(); void update(const std::shared_ptr>& navMesh, std::size_t id, const DetourNavigator::Settings& settings); void reset(); void enable(); void disable(); 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; Settings::NavMeshRenderMode mMode; std::size_t mId; DetourNavigator::Version mVersion; std::map mTiles; std::vector> mWorkItems; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/npcanimation.cpp000066400000000000000000001450671503074453300237140ustar00rootroot00000000000000#include "npcanimation.hpp" #include #include #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 "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "actorutil.hpp" #include "postprocessor.hpp" #include "renderbin.hpp" #include "rotatecontroller.hpp" #include "vismask.hpp" namespace { 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)); if (sVampireMapping.find(thisCombination) == sVampireMapping.end()) { 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 Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodyPart->mModel)).value(); } } namespace MWRender { class HeadAnimationTime : public SceneUtil::ControllerSource { private: MWWorld::Ptr mReference; float mTalkStart; float mTalkStop; float mBlinkStart; float mBlinkStop; float mBlinkTimer; bool mEnabled; float mValue; private: void resetBlinkTimer(); public: HeadAnimationTime(const MWWorld::Ptr& reference); void updatePtr(const MWWorld::Ptr& updated); void update(float dt); void setEnabled(bool enabled); void setTalkStart(float value); void setTalkStop(float value); void setBlinkStart(float value); void setBlinkStop(float value); float getValue(osg::NodeVisitor* nv) override; }; // -------------------------------------------------------------------------------------------------------------- HeadAnimationTime::HeadAnimationTime(const MWWorld::Ptr& reference) : mReference(reference) , mTalkStart(0) , mTalkStop(0) , mBlinkStart(0) , mBlinkStop(0) , mEnabled(true) , mValue(0) { resetBlinkTimer(); } void HeadAnimationTime::updatePtr(const MWWorld::Ptr& updated) { mReference = updated; } void HeadAnimationTime::setEnabled(bool enabled) { mEnabled = enabled; } 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; if (dt == 0.f) return; if (!MWBase::Environment::get().getSoundManager()->sayActive(mReference)) { mBlinkTimer += dt; float duration = mBlinkStop - mBlinkStart; if (mBlinkTimer >= 0 && mBlinkTimer <= duration) { mValue = mBlinkStart + mBlinkTimer; } else mValue = mBlinkStop; 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) } } float HeadAnimationTime::getValue(osg::NodeVisitor*) { return mValue; } void HeadAnimationTime::setTalkStart(float value) { mTalkStart = value; } void HeadAnimationTime::setTalkStop(float value) { mTalkStop = value; } void HeadAnimationTime::setBlinkStart(float value) { mBlinkStart = value; } void HeadAnimationTime::setBlinkStop(float value) { mBlinkStop = 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); } 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; } 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(); 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; mHeadAnimationTime = std::make_shared(mPtr); mWeaponAnimationTime = std::make_shared(this); for (size_t i = 0; i < ESM::PRT_Count; i++) { mPartslots[i] = -1; // each slot is empty mPartPriorities[i] = 0; } std::fill(mSounds.begin(), mSounds.end(), nullptr); updateNpcBase(); } 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(); if (viewChange && Settings::game().mShieldSheathing) { int weaptype = ESM::Weapon::None; MWMechanics::getActiveWeapon(mPtr, &weaptype); showCarriedLeft(updateCarriedLeftVisible(weaptype)); } } /// @brief A RenderBin callback to clear the depth buffer before rendering. /// Switches depth attachments to a proxy renderbuffer, reattaches original depth then redraws first person root. /// This gives a complete depth buffer which can be used for postprocessing, buffer resolves as if depth was never /// cleared. class DepthClearCallback : public osgUtil::RenderBin::DrawCallback { public: DepthClearCallback() { mDepth = new SceneUtil::AutoDepth; mDepth->setWriteMask(true); mStateSet = new osg::StateSet; mStateSet->setAttributeAndModes(new osg::ColorMask(false, false, false, false), osg::StateAttribute::ON); mStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF | osg::StateAttribute::OVERRIDE); } void drawImplementation( osgUtil::RenderBin* bin, osg::RenderInfo& renderInfo, osgUtil::RenderLeaf*& previous) override { osg::State* state = renderInfo.getState(); PostProcessor* postProcessor = static_cast(renderInfo.getCurrentCamera()->getUserData()); state->applyAttribute(mDepth); unsigned int frameId = state->getFrameStamp()->getFrameNumber() % 2; postProcessor->getFbo(PostProcessor::FBO_FirstPerson, frameId)->apply(*state); glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); // color accumulation pass bin->drawImplementation(renderInfo, previous); auto primaryFBO = postProcessor->getPrimaryFbo(frameId); primaryFBO->apply(*state); postProcessor->getFbo(PostProcessor::FBO_OpaqueDepth, frameId)->apply(*state); // depth accumulation pass osg::ref_ptr restore = bin->getStateSet(); bin->setStateSet(mStateSet); bin->drawImplementation(renderInfo, previous); bin->setStateSet(restore); primaryFBO->apply(*state); state->checkGLErrors("after DepthClearCallback::drawImplementation"); } osg::ref_ptr mDepth; osg::ref_ptr mStateSet; }; /// 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) { } 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) { 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); } else if (osg::StateSet* stateset = mObjectRoot->getStateSet()) stateset->setRenderBinToInherit(); } void NpcAnimation::rebuild() { mScabbard.reset(); mHolsteredShield.reset(); updateNpcBase(); MWBase::Environment::get().getMechanicsManager()->forceStateUpdate(mPtr); } int NpcAnimation::getSlot(const osg::NodePath& path) const { for (int i = 0; i < ESM::PRT_Count; ++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; } 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().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(); 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 = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(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 = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(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; bool is1stPerson = mViewMode == VM_FirstPerson; bool isBeast = (race->mData.mFlags & ESM::Race::Beast) != 0; std::string_view base; if (!isWerewolf) { if (!is1stPerson) base = Settings::models().mXbaseanim.get().value(); else base = Settings::models().mXbaseanim1st.get().value(); } const std::string defaultSkeleton = Misc::ResourceHelpers::correctActorModelPath( VFS::Path::toNormalized(getActorSkeleton(is1stPerson, isFemale, isBeast, isWerewolf)), mResourceSystem->getVFS()); std::string smodel = defaultSkeleton; bool isCustomModel = false; if (!is1stPerson && !isWerewolf && !mNpc->mModel.empty()) { VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(mNpc->mModel)); isCustomModel = !isDefaultActorSkeleton(model); smodel = Misc::ResourceHelpers::correctActorModelPath(model, mResourceSystem->getVFS()); } setObjectRoot(smodel, true, true, false); updateParts(); if (!base.empty()) addAnimSource(base, smodel); if (defaultSkeleton != base) addAnimSource(defaultSkeleton, smodel); if (isCustomModel) addAnimSource(smodel, smodel); const bool customArgonianSwim = !is1stPerson && !isWerewolf && isBeast && mNpc->mRace.contains("argonian"); if (customArgonianSwim) addAnimSource(Settings::models().mXargonianswimkna.get().value(), smodel); if (is1stPerson) { mObjectRoot->setNodeMask(Mask_FirstPerson); mObjectRoot->addCullCallback(new OverrideFieldOfViewCallback(mFirstPersonFieldOfView)); } mWeaponAnimationTime->updateStartTime(); } std::string NpcAnimation::getSheathedShieldMesh(const MWWorld::ConstPtr& shield) const { std::string mesh = getShieldMesh(shield, !mNpc->isMale()); if (mesh.empty()) return std::string(); 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; } void NpcAnimation::updateParts() { if (!mObjectRoot.get()) return; NpcType curType = getNpcType(); if (curType != mNpcType) { mNpcType = curType; rebuild(); 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]); bool wasArrowAttached = isArrowAttached(); mAmmunition.reset(); 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); removePartGroup(slotlist[i].mSlot); if (store == inv.end()) continue; if (slotlist[i].mSlot == MWWorld::InventoryStore::Slot_Helmet) removeIndividualPart(ESM::PRT_Hair); 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); } } 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 (mPartPriorities[ESM::PRT_Shield] < 1) { 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(VFS::Path::Normalized(light->mModel)), false, nullptr, true); if (mObjectParts[ESM::PRT_Shield]) addExtraLight(mObjectParts[ESM::PRT_Shield]->getNode()->asGroup(), SceneUtil::LightCommon(*light)); } } showWeapons(mShowWeapons); showCarriedLeft(mShowCarriedLeft); 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) { if (const ESM::BodyPart* bodypart = parts[part]) addOrReplaceIndividualPart(static_cast(part), -1, 1, Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(bodypart->mModel))); } } if (wasArrowAttached) attachArrow(); } PartHolderPtr NpcAnimation::insertBoundedPart(VFS::Path::NormalizedView 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(std::move(attached)); } osg::Vec3f NpcAnimation::runAnimation(float timepassed) { osg::Vec3f ret = Animation::runAnimation(timepassed); mHeadAnimationTime->update(timepassed); if (mFirstPersonNeckController) { if (mAccurateAiming) mAimingFactor = 1.f; else mAimingFactor = std::max(0.f, mAimingFactor - timepassed * 0.5f); float rotateFactor = 0.75f + 0.25f * mAimingFactor; mFirstPersonNeckController->setRotate( osg::Quat(mPtr.getRefData().getPosition().rot[0] * rotateFactor, osg::Vec3f(-1, 0, 0))); mFirstPersonNeckController->setOffset(mFirstPersonOffset); } 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) { MWBase::Environment::get().getSoundManager()->stopSound(mSounds[type]); mSounds[type] = nullptr; } } void NpcAnimation::reserveIndividualPart(ESM::PartReferenceType type, int group, int priority) { if (priority > mPartPriorities[type]) { removeIndividualPart(type); mPartPriorities[type] = priority; mPartslots[type] = group; } } void NpcAnimation::removePartGroup(int group) { for (int i = 0; i < ESM::PRT_Count; i++) { if (mPartslots[i] == group) removeIndividualPart((ESM::PartReferenceType)i); } } 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, VFS::Path::NormalizedView mesh, bool enchantedGlow, osg::Vec4f* glowColor, bool isLight) { if (priority <= mPartPriorities[type]) return false; removeIndividualPart(type); mPartslots[type] = group; mPartPriorities[type] = priority; try { std::string_view bonename = sPartList.at(type); if (type == ESM::PRT_Weapon) { 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) { 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_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 && group == MWWorld::InventoryStore::Slot_CarriedLeft) { const MWWorld::InventoryStore& inv = mPtr.getClass().getInventoryStore(mPtr); MWWorld::ConstContainerStoreIterator csi = inv.getSlot(group); if (csi != inv.end()) { 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) { src = mHeadAnimationTime; if (node->getUserDataContainer()) { for (unsigned int i = 0; i < node->getUserDataContainer()->getNumUserObjects(); ++i) { osg::Object* obj = node->getUserDataContainer()->getUserObject(i); if (SceneUtil::TextKeyMapHolder* keys = dynamic_cast(obj)) { 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; } } } 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); } } 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(); 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(VFS::Path::Normalized(bodypart->mModel)), enchantedGlow, glowColor); else reserveIndividualPart((ESM::PartReferenceType)part.mPart, group, priority); } } void NpcAnimation::addControllers() { Animation::addControllers(); mFirstPersonNeckController = nullptr; WeaponAnimation::deleteControllers(); if (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) { 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 (mViewMode == VM_Normal) { 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()) { osg::Vec4f glowColor = weapon->getClass().getEnchantmentColor(*weapon); const VFS::Path::Normalized 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 { 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); } updateHolsteredWeapon(!mShowWeapons); updateQuiver(); } bool NpcAnimation::updateCarriedLeftVisible(const int weaptype) const { if (Settings::game().mShieldSheathing) { 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); } 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()) { osg::Vec4f glowColor = iter->getClass().getEnchantmentColor(*iter); VFS::Path::Normalized 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)) { 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_Shield); updateHolsteredShield(mShowCarriedLeft); } void NpcAnimation::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)); } updateQuiver(); } void NpcAnimation::detachArrow() { WeaponAnimation::detachArrow(mPtr); 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; 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; int type = weapon->get()->mBase->mData.mType; int ammoType = MWMechanics::getWeaponType(type)->mAmmoType; if (ammoType == ESM::Weapon::None) return nullptr; // 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; } osg::Node* NpcAnimation::getWeaponNode() { const PartHolder* const part = mObjectParts[ESM::PRT_Weapon].get(); if (part == nullptr) return nullptr; return part->getNode(); } Resource::ResourceSystem* NpcAnimation::getResourceSystem() { return mResourceSystem; } void NpcAnimation::enableHeadAnimation(bool enable) { mHeadAnimationTime->setEnabled(enable); } void NpcAnimation::setWeaponGroup(const std::string& group, bool relativeDuration) { mWeaponAnimationTime->setGroup(group, relativeDuration); } void NpcAnimation::equipmentChanged() { if (Settings::game().mShieldSheathing) { int weaptype = ESM::Weapon::None; MWMechanics::getActiveWeapon(mPtr, &weaptype); showCarriedLeft(updateCarriedLeftVisible(weaptype)); } updateParts(); } void NpcAnimation::setVampire(bool vampire) { 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(); } } void NpcAnimation::setFirstPersonOffset(const osg::Vec3f& offset) { mFirstPersonOffset = offset; } 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::vector> RaceMapping; static RaceMapping sRaceMapping; const std::vector& NpcAnimation::getBodyParts( const ESM::RefId& 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 { std::vector& parts = sRaceMapping[std::make_pair(race, flags)]; 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 } }; parts.resize(ESM::PRT_Count, nullptr); if (werewolf) return parts; const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore(); 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; if (!(bodypart.mRace == race)) continue; const bool partFirstPerson = ESM::isFirstPersonBodyPart(bodypart); 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; bool isSameGender = isFemalePart(&bodypart) == female; /* 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) { // 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 no fallback bodypart and searching for female bodyparts (3) else if (!parts[bIt->second] && female) parts[bIt->second] = &bodypart; ++bIt; } 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 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 && !ESM::isFirstPersonBodyPart(*parts[bIt->second]) && partFirstPerson) parts[bIt->second] = &bodypart; ++bIt; } 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; } } return parts; } } void NpcAnimation::setAccurateAiming(bool enabled) { mAccurateAiming = enabled; } bool NpcAnimation::isArrowAttached() const { return mAmmunition != nullptr; } } openmw-openmw-0.49.0/apps/openmw/mwrender/npcanimation.hpp000066400000000000000000000144521503074453300237120ustar00rootroot00000000000000#ifndef GAME_RENDER_NPCANIMATION_H #define GAME_RENDER_NPCANIMATION_H #include "actoranimation.hpp" #include "animation.hpp" #include "weaponanimation.hpp" #include #include "../mwworld/inventorystore.hpp" #include namespace ESM { struct NPC; struct BodyPart; } namespace MWSound { class Sound; } namespace MWRender { class RotateController; class HeadAnimationTime; class NpcAnimation : public ActorAnimation, public WeaponAnimation, public MWWorld::InventoryStoreListener { public: void equipmentChanged() override; public: typedef std::map PartBoneMap; enum ViewMode { VM_Normal, VM_FirstPerson, VM_HeadOnly }; private: static const PartBoneMap sPartList; // Bounded Parts PartHolderPtr mObjectParts[ESM::PRT_Count]; std::array mSounds; const ESM::NPC* mNpc; VFS::Path::Normalized mHeadModel; VFS::Path::Normalized mHairModel; ViewMode mViewMode; bool mShowWeapons; bool mShowCarriedLeft; 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]; osg::Vec3f mFirstPersonOffset; // Field of view to use when rendering first person meshes float mFirstPersonFieldOfView; std::shared_ptr mHeadAnimationTime; std::shared_ptr mWeaponAnimationTime; bool mSoundsDisabled; bool mAccurateAiming; float mAimingFactor; void updateNpcBase(); NpcType getNpcType() const; PartHolderPtr insertBoundedPart(VFS::Path::NormalizedView 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); bool addOrReplaceIndividualPart(ESM::PartReferenceType type, int group, int priority, VFS::Path::NormalizedView 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(); osg::ref_ptr mFirstPersonNeckController; static bool isFemalePart(const ESM::BodyPart* bodypart); static NpcType getNpcType(const MWWorld::Ptr& ptr); 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(); 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; void setWeaponGroup(const std::string& group, bool relativeDuration) 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; } bool getWeaponsShown() const override { return mShowWeapons; } void showWeapons(bool showWeapon) 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; osg::Group* getArrowBone() override; osg::Node* getWeaponNode() override; Resource::ResourceSystem* getResourceSystem() override; // WeaponAnimation void showWeapon(bool show) override { showWeapons(show); } void setViewMode(ViewMode viewMode); void updateParts(); /// 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; 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); 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 ESM::RefId& raceId, bool female, bool firstperson, bool werewolf); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/objectpaging.cpp000066400000000000000000001250661503074453300236650ustar00rootroot00000000000000#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 "apps/openmw/mwbase/environment.hpp" #include "apps/openmw/mwbase/world.hpp" #include "apps/openmw/mwworld/esmstore.hpp" #include "vismask.hpp" namespace MWRender { namespace { bool typeFilter(int type, bool far) { 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, ESM::RefId id, const MWWorld::ESMStore& store) { 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) { if (activeGrid && !mActiveGrid) return nullptr; const ChunkId id = std::make_tuple(center, size, activeGrid); 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; } namespace { class CanOptimizeCallback : public SceneUtil::Optimizer::IsOperationPermissibleForObjectCallback { 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 { std::max(left.first, right.first), std::min(left.second, right.second) }; } bool empty(const LODRange& r) { return r.first >= r.second; } LODRange operator/(const LODRange& r, float div) { return { r.first / div, r.second / div }; } class CopyOp : public osg::CopyOp { public: bool mOptimizeBillboards = true; bool mActiveGrid = false; LODRange mDistances = { 0.f, 0.f }; osg::Vec3f mViewVector; osg::Node::NodeMask mCopyMask = ~0u; mutable std::vector mNodePath; CopyOp(bool activeGrid, osg::Node::NodeMask copyMask) : mActiveGrid(activeGrid) , mCopyMask(copyMask) { } void copy(const osg::Node* toCopy, osg::Group* attachTo) { 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))); } } osg::Node* operator()(const osg::Node* node) const override { 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; 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; } mNodePath.push_back(node); osg::Node* cloned = static_cast(node->clone(*this)); if (!mActiveGrid) 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 { for (const osg::Callback* callback = node->getCullCallback(); callback != nullptr; callback = callback->getNestedCallback()) { if (callback->className() == std::string("BillboardCallback")) { 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->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 (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::Drawable* d = static_cast(drawable->clone(*this)); d->setDataVariance(osg::Object::STATIC); d->setUserDataContainer(nullptr); d->setName(""); return d; } 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::vector mRefnums; }; class AnalyzeVisitor : public osg::NodeVisitor { public: AnalyzeVisitor(osg::Node::NodeMask analyzeMask) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mCurrentStateSet(nullptr) { setTraversalMask(analyzeMask); } typedef std::unordered_map StateSetCounter; struct Result { StateSetCounter mStateSetCounter; unsigned int mNumVerts = 0; }; void apply(osg::Node& node) override { if (node.getStateSet()) mCurrentStateSet = node.getStateSet(); 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 { 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; } Result mResult; osg::StateSet* mCurrentStateSet; StateSetCounter mGlobalStateSetCounter; LODRange mDistances = { 0.f, 0.f }; }; class DebugVisitor : public osg::NodeVisitor { public: DebugVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) { } void apply(osg::Drawable& node) override { 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); } }; class AddRefnumMarkerVisitor : public osg::NodeVisitor { public: AddRefnumMarkerVisitor(ESM::RefNum refnum) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mRefnum(refnum) { } 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); } }; } 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) { } namespace { struct PagedCellRef { ESM::RefId mRefId; ESM::RefNum mRefNum; osg::Vec3f mPosition; osg::Vec3f mRotation; float mScale; }; PagedCellRef makePagedCellRef(const ESM::CellRef& value) { return PagedCellRef{ .mRefId = value.mRefID, .mRefNum = value.mRefNum, .mPosition = value.mPos.asVec3(), .mRotation = value.mPos.asRotationVec3(), .mScale = value.mScale, }; } std::map collectESM3References( float size, const osg::Vec2i& startCell, const MWWorld::ESMStore& store) { std::map refs; ESM::ReadersCache readers; 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; for (size_t i = 0; i < cell->mContextList.size(); ++i) { 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) { Log(Debug::Warning) << "Failed to collect references from cell \"" << cell->getDescription() << "\": " << e.what(); continue; } } for (const auto& [ref, deleted] : cell->mLeasedRefs) { 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)); } } } return refs; } } 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); 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); }); } } 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; AnalyzeVisitor::Result mAnalyzeResult; bool mNeedCompile = false; }; typedef std::map, InstanceList> NodeMap; NodeMap nodes; 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. // 2. For nodes masked via Flag_Hidden (VisController can change this flag value at runtime). // 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); const float minSize = mMinSizeMergeFactor ? mMinSize * mMinSizeMergeFactor : mMinSize; for (const auto& [refNum, ref] : refs) { if (size < 1.f) { 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; } const float dSqr = (viewPoint - ref.mPosition).length2(); if (!activeGrid) { std::lock_guard lock(mSizeCacheMutex); SizeCache::iterator found = mSizeCache.find(refNum); if (found != mSizeCache.end() && found->second < dSqr * minSize * minSize) continue; } if (Misc::ResourceHelpers::isHiddenMarker(ref.mRefId)) continue; const int type = store.findStatic(ref.mRefId); VFS::Path::Normalized 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()); if (Misc::getFileExtension(model) == "nif") { VFS::Path::Normalized kfname = model; kfname.changeExtension("kf"); if (mSceneManager->getVFS()->exists(kfname)) continue; } } 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(model, false); if (activeGrid) { 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.push_back(refNum); } { std::lock_guard lock(mRefTrackerMutex); if (getRefTracker().mDisabled.count(refNum)) continue; } const float radius2 = cnode->getBound().radius2() * ref.mScale * ref.mScale; if (radius2 < dSqr * minSize * minSize && !activeGrid) { std::lock_guard lock(mSizeCacheMutex); mSizeCache[refNum] = radius2; continue; } const auto emplaced = nodes.emplace(std::move(cnode), InstanceList()); if (emplaced.second) { 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 && 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; osgUtil::StateToCompile stateToCompile(0, nullptr); CopyOp copyop(activeGrid, copyMask); for (const auto& pair : nodes) { const osg::Node* cnode = pair.first; const AnalyzeVisitor::Result& analyzeResult = pair.second.mAnalyzeResult; const float mergeCost = analyzeResult.mNumVerts * size; const float mergeBenefit = analyzeVisitor.getMergeBenefit(analyzeResult) * mMergeFactor; const bool merge = mergeBenefit > mergeCost; 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 (const PagedCellRef* refPtr : pair.second.mInstances) { const PagedCellRef& ref = *refPtr; if (!activeGrid && minSizeMerged != minSize && cnode->getBound().radius2() * ref.mScale * ref.mScale < (viewPoint - ref.mPosition).length2() * minSizeMerged * minSizeMerged) continue; 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.mDistances = LODRange{ smallestDistanceToChunk, higherDistanceToChunk } / ref.mScale; copyop.mViewVector = (viewPoint - worldCenter); copyop.copy(cnode, trans); copyop.mNodePath.pop_back(); if (activeGrid) { if (merge) { AddRefnumMarkerVisitor visitor(ref.mRefNum); trans->accept(visitor); } else { osg::ref_ptr marker = new RefnumMarker; marker->mRefnum = ref.mRefNum; trans->getOrCreateUserDataContainer()->addUserObject(marker); } } osg::Group* const attachTo = merge ? mergeGroup : group; attachTo->addChild(trans); ++numinstances; } if (numinstances > 0) { // 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) { int mode = osgUtil::GLObjectsVisitor::COMPILE_STATE_ATTRIBUTES; if (!merge) mode |= osgUtil::GLObjectsVisitor::COMPILE_DISPLAY_LISTS; stateToCompile._mode = mode; const_cast(cnode)->accept(stateToCompile); } } } const osg::Vec3f relativeViewPoint = viewPoint - worldCenter; if (mergeGroup->getNumChildren()) { SceneUtil::Optimizer optimizer; if (size > 1 / 8.f) { optimizer.setViewPoint(relativeViewPoint); optimizer.setMergeAlphaBlending(true); } optimizer.setIsOperationPermissibleForObjectCallback(new CanOptimizeCallback); 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); if (mDebugBatches) { DebugVisitor dv; mergeGroup->accept(dv); } if (compile) { stateToCompile._mode = osgUtil::GLObjectsVisitor::COMPILE_DISPLAY_LISTS; mergeGroup->accept(stateToCompile); } } osgUtil::IncrementalCompileOperation* const ico = mSceneManager->getIncrementalCompileOperation(); if (!stateToCompile.empty() && ico) { auto compileSet = new osgUtil::IncrementalCompileOperation::CompileSet(group); compileSet->buildCompileMap(ico->getContextSet(), stateToCompile); ico->add(compileSet, false); } group->getBound(); group->setNodeMask(Mask_Static); 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); } udc->addUserObject(templateRefs); return group; } unsigned int ObjectPaging::getNodeMask() { return Mask_Static; } namespace { osg::Vec2f clampToCell(const osg::Vec3f& cellPos, const osg::Vec2i& cell) { return osg::Vec2f(std::clamp(cellPos.x(), cell.x(), cell.x() + 1), std::clamp(cellPos.y(), cell.y(), cell.y() + 1)); } class CollectIntersecting { public: explicit CollectIntersecting( bool activeGridOnly, const osg::Vec3f& position, const osg::Vec2i& cell, ESM::RefId worldspace) : mActiveGridOnly(activeGridOnly) , mPosition(clampToCell(position / getCellSize(worldspace), cell)) { } 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; } CollectIntersecting ccf(false, pos, cell, mWorldspace); mCache->call(ccf); if (ccf.getCollected().empty()) return false; for (const ChunkId& chunk : ccf.getCollected()) mCache->removeFromObjectCache(chunk); return true; } 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; } CollectIntersecting ccf(true, pos, cell, mWorldspace); mCache->call(ccf); 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); mRefTrackerNew.mDisabled.clear(); mRefTrackerNew.mBlacklist.clear(); mRefTrackerLocked = true; } bool ObjectPaging::unlockCache() { if (!mRefTrackerLocked) return false; { std::lock_guard lock(mRefTrackerMutex); mRefTrackerLocked = false; if (mRefTracker == mRefTrackerNew) return false; else mRefTracker = mRefTrackerNew; } mCache->clear(); return true; } namespace { struct GetRefnumsFunctor { GetRefnumsFunctor(std::vector& output) : mOutput(output) { } 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::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 { Resource::reportStats("Object Chunk", frameNumber, mCache->getStats(), *stats); } } openmw-openmw-0.49.0/apps/openmw/mwrender/objectpaging.hpp000066400000000000000000000065671503074453300236760ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_OBJECTPAGING_H #define OPENMW_MWRENDER_OBJECTPAGING_H #include #include #include #include namespace Resource { class SceneManager; } namespace MWRender { typedef std::tuple ChunkId; // Center, Size, ActiveGrid class ObjectPaging : public Resource::GenericResourceManager, public Terrain::QuadTreeWorld::ChunkManager { public: 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 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, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell, bool enabled); /// @return true if view needs rebuild bool blacklistObject(int type, ESM::RefNum refnum, const osg::Vec3f& pos, const osg::Vec2i& cell); void clear(); /// Must be called after clear() before rendering starts. /// @return true if view needs rebuild bool unlockCache(); void reportStats(unsigned int frameNumber, osg::Stats* stats) const override; void getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out); private: Resource::SceneManager* mSceneManager; bool mActiveGrid; bool mDebugBatches; float mMergeFactor; float mMinSize; float mMinSizeMergeFactor; float mMinSizeCostMultiplier; std::mutex mRefTrackerMutex; struct RefTracker { std::set mDisabled; std::set mBlacklist; bool operator==(const RefTracker& other) const { return mDisabled == other.mDisabled && mBlacklist == other.mBlacklist; } }; RefTracker mRefTracker; RefTracker mRefTrackerNew; bool mRefTrackerLocked; const RefTracker& getRefTracker() const { return mRefTracker; } RefTracker& getWritableRefTracker() { return mRefTrackerLocked ? mRefTrackerNew : mRefTracker; } std::mutex mSizeCacheMutex; typedef std::map SizeCache; SizeCache mSizeCache; std::mutex mLODNameCacheMutex; typedef std::pair LODNameCacheKey; // Key: mesh name, lod level using LODNameCache = std::map; // Cache: key, mesh name to use LODNameCache mLODNameCache; }; class RefnumMarker : public osg::Object { public: RefnumMarker() : mNumVertices(0) { } RefnumMarker(const RefnumMarker& copy, osg::CopyOp co) : mRefnum(copy.mRefnum) , mNumVertices(copy.mNumVertices) { } META_Object(MWRender, RefnumMarker) ESM::RefNum mRefnum; unsigned int mNumVertices; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/objects.cpp000066400000000000000000000206511503074453300226540ustar00rootroot00000000000000#include "objects.hpp" #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "animation.hpp" #include "creatureanimation.hpp" #include "esm4npcanimation.hpp" #include "npcanimation.hpp" #include "vismask.hpp" namespace MWRender { Objects::Objects(Resource::ResourceSystem* resourceSystem, const 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(); } void Objects::insertBegin(const MWWorld::Ptr& ptr) { assert(mObjects.find(ptr.mRef) == mObjects.end()); osg::ref_ptr cellnode; 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; osg::ref_ptr insert(new SceneUtil::PositionAttitudeTransform); cellnode->addChild(insert); insert->getOrCreateUserDataContainer()->addUserObject(new PtrHolder(ptr)); const float* f = ptr.getRefData().getPosition().pos; insert->setPosition(osg::Vec3(f[0], f[1], f[2])); const float scale = ptr.getCellRef().getScale(); osg::Vec3f scaleVec(scale, scale, scale); ptr.getClass().adjustScale(ptr, scaleVec, true); insert->setScale(scaleVec); ptr.getRefData().setBaseNode(std::move(insert)); } 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( VFS::Path::toNormalized(mesh), mResourceSystem->getVFS()); if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) animated = false; } osg::ref_ptr anim( new ObjectAnimation(ptr, animationMesh, mResourceSystem, animated, allowLight)); mObjects.emplace(ptr.mRef, std::move(anim)); } void Objects::insertCreature(const MWWorld::Ptr& ptr, const std::string& mesh, bool weaponsShields) { insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); bool animated = true; std::string animationMesh = Misc::ResourceHelpers::correctActorModelPath(VFS::Path::toNormalized(mesh), mResourceSystem->getVFS()); if (animationMesh == mesh && Misc::StringUtils::ciEndsWith(animationMesh, ".nif")) animated = false; // CreatureAnimation osg::ref_ptr anim; if (weaponsShields) anim = new CreatureWeaponAnimation(ptr, animationMesh, mResourceSystem, animated); else anim = new CreatureAnimation(ptr, animationMesh, mResourceSystem, animated); if (mObjects.emplace(ptr.mRef, anim).second) ptr.getClass().getContainerStore(ptr).setContListener(static_cast(anim.get())); } void Objects::insertNPC(const MWWorld::Ptr& ptr) { insertBegin(ptr); ptr.getRefData().getBaseNode()->setNodeMask(Mask_Actor); if (ptr.getType() == ESM::REC_NPC_4) { 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)); if (mObjects.emplace(ptr.mRef, anim).second) { ptr.getClass().getInventoryStore(ptr).setInvListener(anim.get()); ptr.getClass().getInventoryStore(ptr).setContListener(anim.get()); } } } bool Objects::removeObject(const MWWorld::Ptr& ptr) { if (!ptr.getRefData().getBaseNode()) return true; const auto iter = mObjects.find(ptr.mRef); if (iter != mObjects.end()) { iter->second->removeFromScene(); mUnrefQueue.push(std::move(iter->second)); mObjects.erase(iter); if (ptr.getClass().isActor()) { if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).setInvListener(nullptr); ptr.getClass().getContainerStore(ptr).setContListener(nullptr); } ptr.getRefData().getBaseNode()->getParent(0)->removeChild(ptr.getRefData().getBaseNode()); ptr.getRefData().setBaseNode(nullptr); return true; } return false; } void Objects::removeCell(const MWWorld::CellStore* store) { 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; } CellMap::iterator cell = mCellSceneNodes.find(store); if (cell != mCellSceneNodes.end()) { cell->second->getParent(0)->removeChild(cell->second); mCellSceneNodes.erase(cell); } } 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 { cellnode = mCellSceneNodes[newCell]; } 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)); } 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.mRef); if (iter != mObjects.end()) return iter->second; return nullptr; } 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; } } openmw-openmw-0.49.0/apps/openmw/mwrender/objects.hpp000066400000000000000000000043541503074453300226630ustar00rootroot00000000000000#ifndef GAME_RENDER_OBJECTS_H #define GAME_RENDER_OBJECTS_H #include #include #include #include #include "../mwworld/ptr.hpp" namespace osg { class Group; } namespace Resource { class ResourceSystem; } namespace MWWorld { class CellStore; } namespace SceneUtil { class UnrefQueue; } namespace MWRender { class Animation; class PtrHolder : public osg::Object { public: PtrHolder(const MWWorld::Ptr& ptr) : mPtr(ptr) { } PtrHolder() {} PtrHolder(const PtrHolder& copy, const osg::CopyOp& copyop) : mPtr(copy.mPtr) { } META_Object(MWRender, PtrHolder) MWWorld::Ptr mPtr; }; class Objects { using PtrAnimationMap = std::map>; typedef std::map> CellMap; CellMap mCellSceneNodes; PtrAnimationMap mObjects; osg::ref_ptr mRootNode; Resource::ResourceSystem* mResourceSystem; SceneUtil::UnrefQueue& mUnrefQueue; void insertBegin(const MWWorld::Ptr& ptr); public: Objects(Resource::ResourceSystem* resourceSystem, const osg::ref_ptr& rootNode, SceneUtil::UnrefQueue& unrefQueue); ~Objects(); /// @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); Animation* getAnimation(const MWWorld::Ptr& ptr); const Animation* getAnimation(const MWWorld::ConstPtr& ptr) const; bool removeObject(const MWWorld::Ptr& ptr); ///< \return found? void removeCell(const MWWorld::CellStore* store); /// 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&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/pathgrid.cpp000066400000000000000000000112531503074453300230230ustar00rootroot00000000000000#include "pathgrid.hpp" #include #include #include #include #include #include #include #include #include #include #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 "vismask.hpp" namespace MWRender { Pathgrid::Pathgrid(osg::ref_ptr root) : mPathgridEnabled(false) , mRootNode(std::move(root)) , mPathGridRoot(nullptr) , mInteriorPathgridNode(nullptr) { } Pathgrid::~Pathgrid() { if (mPathgridEnabled) { try { togglePathgrid(); } catch (std::exception& e) { Log(Debug::Error) << "Failed to destroy pathgrid: " << e.what(); } } } bool Pathgrid::toggleRenderMode(int mode) { switch (mode) { case Render_Pathgrid: togglePathgrid(); return mPathgridEnabled; default: return false; } return false; } void Pathgrid::addCell(const MWWorld::CellStore* store) { mActiveCells.push_back(store); if (mPathgridEnabled) enableCellPathgrid(store); } void Pathgrid::removeCell(const MWWorld::CellStore* store) { mActiveCells.erase(std::remove(mActiveCells.begin(), mActiveCells.end(), store), mActiveCells.end()); if (mPathgridEnabled) disableCellPathgrid(store); } void Pathgrid::togglePathgrid() { mPathgridEnabled = !mPathgridEnabled; if (mPathgridEnabled) { // 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); } } else { // 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; osg::Vec3f cellPathGridPos(0, 0, 0); Misc::makeCoordinateConverter(*store->getCell()).toWorld(cellPathGridPos); osg::ref_ptr cellPathGrid = new osg::PositionAttitudeTransform; cellPathGrid->setPosition(cellPathGridPos); osg::ref_ptr geometry = SceneUtil::createPathgridGeometry(*pathgrid); MWBase::Environment::get().getResourceSystem()->getSceneManager()->recreateShaders(geometry, "debug"); cellPathGrid->addChild(geometry); mPathGridRoot->addChild(cellPathGrid); if (store->getCell()->isExterior()) { mExteriorPathgridNodes[std::make_pair(store->getCell()->getGridX(), store->getCell()->getGridY())] = cellPathGrid; } else { assert(mInteriorPathgridNode == nullptr); mInteriorPathgridNode = cellPathGrid; } } 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->removeChild(it->second); mExteriorPathgridNodes.erase(it); } } else { if (mInteriorPathgridNode) { mPathGridRoot->removeChild(mInteriorPathgridNode); mInteriorPathgridNode = nullptr; } } } } openmw-openmw-0.49.0/apps/openmw/mwrender/pathgrid.hpp000066400000000000000000000022461503074453300230320ustar00rootroot00000000000000#ifndef GAME_RENDER_MWSCENE_H #define GAME_RENDER_MWSCENE_H #include #include #include #include namespace ESM { struct Pathgrid; } namespace osg { class Group; class Geometry; } namespace MWWorld { class Ptr; class CellStore; } namespace MWRender { class Pathgrid { bool mPathgridEnabled; void togglePathgrid(); typedef std::vector CellList; CellList mActiveCells; osg::ref_ptr mRootNode; osg::ref_ptr mPathGridRoot; 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); public: Pathgrid(osg::ref_ptr root); ~Pathgrid(); bool toggleRenderMode(int mode); void addCell(const MWWorld::CellStore* store); void removeCell(const MWWorld::CellStore* store); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/pingpongcanvas.cpp000066400000000000000000000324201503074453300242350ustar00rootroot00000000000000#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(); } } openmw-openmw-0.49.0/apps/openmw/mwrender/pingpongcanvas.hpp000066400000000000000000000053151503074453300242450ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/pingpongcull.cpp000066400000000000000000000056031503074453300237240ustar00rootroot00000000000000#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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/pingpongcull.hpp000066400000000000000000000013751503074453300237330ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/postprocessor.cpp000066400000000000000000001015641503074453300241530ustar00rootroot00000000000000#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; fbos[FBO_Intercept] = 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); fbos[FBO_Intercept]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER1, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Normal])); } 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]->setAttachment(osg::FrameBufferObject::BufferComponent::COLOR_BUFFER0, Stereo::createMultiviewCompatibleAttachment(textures[Tex_Scene])); } 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; std::string realName = name; auto fileIter = mTechniqueFileMap.find(name); if (fileIter != mTechniqueFileMap.end()) realName = fileIter->first; auto technique = std::make_shared(*mVFS, *mRendering.getResourceSystem()->getImageManager(), std::move(realName), renderWidth(), renderHeight(), mUBO, mNormalsSupported); technique->compile(); if (technique->getStatus() != fx::Technique::Status::File_Not_exists) technique->setLastModificationTime(std::filesystem::last_write_time(fileIter->second)); if (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; } } openmw-openmw-0.49.0/apps/openmw/mwrender/postprocessor.hpp000066400000000000000000000160601503074453300241540ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_POSTPROCESSOR_H #define OPENMW_MWRENDER_POSTPROCESSOR_H #include #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 openmw-openmw-0.49.0/apps/openmw/mwrender/precipitationocclusion.cpp000066400000000000000000000153631503074453300260200ustar00rootroot00000000000000#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; } } openmw-openmw-0.49.0/apps/openmw/mwrender/precipitationocclusion.hpp000066400000000000000000000015141503074453300260160ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/recastmesh.cpp000066400000000000000000000074101503074453300233570ustar00rootroot00000000000000#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()) { } RecastMesh::~RecastMesh() { if (mEnabled) disable(); } bool RecastMesh::toggle() { if (mEnabled) disable(); else enable(); return mEnabled; } void RecastMesh::update(const DetourNavigator::RecastMeshTiles& tiles, const DetourNavigator::Settings& settings) { if (!mEnabled) return; for (auto it = mGroups.begin(); it != mGroups.end();) { const auto tile = tiles.find(it->first); if (tile == tiles.end()) { mRootNode->removeChild(it->second.mValue); it = mGroups.erase(it); continue; } if (it->second.mVersion != tile->second->getVersion()) { 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.mVersion = tile->second->getVersion(); } ++it; } for (const auto& [position, mesh] : tiles) { 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); 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); }); mGroups.clear(); } void RecastMesh::enable() { 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); }); mEnabled = false; } } openmw-openmw-0.49.0/apps/openmw/mwrender/recastmesh.hpp000066400000000000000000000022171503074453300233640ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_RECASTMESH_H #define OPENMW_MWRENDER_RECASTMESH_H #include #include #include namespace osg { class Group; class Geometry; class StateSet; } namespace DetourNavigator { struct Settings; } namespace MWRender { class RecastMesh { public: RecastMesh(const osg::ref_ptr& root, bool enabled); ~RecastMesh(); bool toggle(); void update(const DetourNavigator::RecastMeshTiles& recastMeshTiles, const DetourNavigator::Settings& settings); void reset(); void enable(); void disable(); bool isEnabled() const { return mEnabled; } private: struct Group { DetourNavigator::Version mVersion; osg::ref_ptr mValue; }; osg::ref_ptr mRootNode; bool mEnabled; std::map mGroups; osg::ref_ptr mGroupStateSet; osg::ref_ptr mDebugDrawStateSet; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/renderbin.hpp000066400000000000000000000011161503074453300231730ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_RENDERBIN_H #define OPENMW_MWRENDER_RENDERBIN_H namespace MWRender { /// Defines the render bin numbers used in the OpenMW scene graph. The bin with the lowest number is rendered first. enum RenderBins { RenderBin_Sky = -1, RenderBin_Default = 0, // osg::StateSet::OPAQUE_BIN RenderBin_Water = 9, RenderBin_DepthSorted = 10, // osg::StateSet::TRANSPARENT_BIN RenderBin_OcclusionQuery = 11, RenderBin_FirstPerson = 12, RenderBin_SunGlare = 13, RenderBin_Distortion = 14, }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/renderinginterface.hpp000066400000000000000000000004371503074453300250660ustar00rootroot00000000000000#ifndef GAME_RENDERING_INTERFACE_H #define GAME_RENDERING_INTERFACE_H namespace MWRender { class Objects; class Actors; class RenderingInterface { public: virtual MWRender::Objects& getObjects() = 0; virtual ~RenderingInterface() {} }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/renderingmanager.cpp000066400000000000000000002147461503074453300245450ustar00rootroot00000000000000#include "renderingmanager.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 "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwworld/groundcoverstore.hpp" #include "../mwworld/scene.hpp" #include "../mwgui/postprocessorhud.hpp" #include "../mwmechanics/actorutil.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 "objectpaging.hpp" #include "pathgrid.hpp" #include "postprocessor.hpp" #include "recastmesh.hpp" #include "screenshotmanager.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 { public: StateUpdater() : mFogStart(0.f) , mFogEnd(0.f) , mWireframe(false) { } void setDefaults(osg::StateSet* stateset) override { osg::LightModel* lightModel = new osg::LightModel; stateset->setAttribute(lightModel, osg::StateAttribute::ON); osg::Fog* fog = new osg::Fog; fog->setMode(osg::Fog::LINEAR); stateset->setAttributeAndModes(fog, osg::StateAttribute::ON); if (mWireframe) { osg::PolygonMode* polygonmode = new osg::PolygonMode; polygonmode->setMode(osg::PolygonMode::FRONT_AND_BACK, osg::PolygonMode::LINE); stateset->setAttributeAndModes(polygonmode, osg::StateAttribute::ON); } else stateset->removeAttribute(osg::StateAttribute::POLYGONMODE); } void apply(osg::StateSet* stateset, osg::NodeVisitor*) override { 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); fog->setStart(mFogStart); fog->setEnd(mFogEnd); } void setAmbientColor(const osg::Vec4f& col) { mAmbientColor = col; } void setFogColor(const osg::Vec4f& col) { mFogColor = col; } void setFogStart(float start) { mFogStart = start; } void setFogEnd(float end) { mFogEnd = end; } void setWireframe(bool wireframe) { if (mWireframe != wireframe) { mWireframe = wireframe; reset(); } } bool getWireframe() const { return mWireframe; } private: osg::Vec4f mAmbientColor; osg::Vec4f mFogColor; float mFogStart; float mFogEnd; bool mWireframe; }; class PreloadCommonAssetsWorkItem : public SceneUtil::WorkItem { public: PreloadCommonAssetsWorkItem(Resource::ResourceSystem* resourceSystem) : mResourceSystem(resourceSystem) { } void doWork() override { try { 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 (const std::exception& e) { Log(Debug::Warning) << "Failed to preload common assets: " << e.what(); } } 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, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore, SceneUtil::UnrefQueue& unrefQueue) : mSkyBlending(Settings::fog().mSkyBlending) , mViewer(viewer) , mRootNode(rootNode) , mResourceSystem(resourceSystem) , mWorkQueue(workQueue) , mNavigator(navigator) , 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) { bool reverseZ = SceneUtil::AutoDepth::isReversed(); const SceneUtil::LightingMethod lightingMethod = Settings::shaders().mLightingMethod; resourceSystem->getSceneManager()->setParticleSystemMask(MWRender::Mask_ParticleSystem); // 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::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()); sceneRoot->setLightingMask(Mask_Lighting); mSceneRoot = sceneRoot; sceneRoot->setStartLight(1); sceneRoot->setNodeMask(Mask_Scene); sceneRoot->setName("Scene Root"); int shadowCastingTraversalMask = Mask_Scene; if (Settings::shadows().mActorShadows) shadowCastingTraversalMask |= Mask_Actor; if (Settings::shadows().mPlayerShadows) shadowCastingTraversalMask |= Mask_Player; int indoorShadowCastingTraversalMask = shadowCastingTraversalMask; if (Settings::shadows().mObjectShadows) shadowCastingTraversalMask |= (Mask_Object | Mask_Static); if (Settings::shadows().mTerrainShadows) shadowCastingTraversalMask |= Mask_Terrain; 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(Settings::shadows()); Shader::ShaderManager::DefineMap lightDefines = sceneRoot->getLightDefines(); 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::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 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(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 = 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 = std::make_unique(mResourceSystem, sceneRoot, unrefQueue); if (getenv("OPENMW_DONT_PRECOMPILE") == nullptr) { mViewer->setIncrementalCompileOperation(new osgUtil::IncrementalCompileOperation); 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 = std::make_unique(sceneRoot, mResourceSystem); 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 = std::make_unique(mResourceSystem, normalMapPattern, heightMapPattern, useTerrainNormalMaps, specularMapPattern, useTerrainSpecularMaps); WorldspaceChunkMgr& chunkMgr = getWorldspaceChunkMgr(ESM::Cell::sDefaultWorldspaceId); mTerrain = chunkMgr.mTerrain.get(); mGroundcover = chunkMgr.mGroundcover.get(); mObjectPaging = chunkMgr.mObjectPaging.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); // water goes after terrain for correct waterculling order mWater = std::make_unique( sceneRoot->getParent(0), sceneRoot, mResourceSystem, mViewer->getIncrementalCompileOperation()); mCamera = std::make_unique(mViewer->getCamera()); mScreenshotManager = std::make_unique(viewer); mViewer->setLightingMode(osgViewer::View::NO_LIGHT); osg::ref_ptr source = new osg::LightSource; 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->setConstantAttenuation(1.f); sceneRoot->setSunlight(mSunLight); sceneRoot->addChild(source); 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); 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)); 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)); resourceSystem->getSceneManager()->setUpNormalsRTForStateSet(sceneRoot->getOrCreateStateSet(), true); mFog = std::make_unique(); 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()); } source->setStateSetModes(*mRootNode->getOrCreateStateSet(), osg::StateAttribute::ON); osg::Camera::CullingMode cullingMode = osg::Camera::DEFAULT_CULLING | osg::Camera::FAR_PLANE_CULLING; if (!Settings::camera().mSmallFeatureCulling) cullingMode &= ~(osg::CullStack::SMALL_FEATURE_CULLING); else { mViewer->getCamera()->setSmallFeatureCullingPixelSize(Settings::camera().mSmallFeatureCullingPixelSize); cullingMode |= osg::CullStack::SMALL_FEATURE_CULLING; } mViewer->getCamera()->setComputeNearFarMode(osg::Camera::DO_NOT_COMPUTE_NEAR_FAR); mViewer->getCamera()->setCullingMode(cullingMode); mViewer->getCamera()->setName(Constants::SceneCamera); auto mask = ~(Mask_UpdateVisitor | Mask_SimpleWater); MWBase::Environment::get().getWindowManager()->setCullMask(mask); NifOsg::Loader::setHiddenNodeMask(Mask_UpdateVisitor); NifOsg::Loader::setIntersectionDisabledNodeMask(Mask_Effect); Nif::Reader::setLoadUnsupportedFiles(Settings::models().mLoadUnsupportedNifFiles); 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. mRootNode->getOrCreateStateSet()->setMode(GL_ALPHA_TEST, osg::StateAttribute::OFF); 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() { // let background loading thread finish before we delete anything else mWorkQueue = nullptr; } osgUtil::IncrementalCompileOperation* RenderingManager::getIncrementalCompileOperation() { return mViewer->getIncrementalCompileOperation(); } MWRender::Objects& RenderingManager::getObjects() { return *mObjects.get(); } Resource::ResourceSystem* RenderingManager::getResourceSystem() { return mResourceSystem; } SceneUtil::WorkQueue* RenderingManager::getWorkQueue() { return mWorkQueue.get(); } Terrain::World* RenderingManager::getTerrain() { return mTerrain; } void RenderingManager::preloadCommonAssets() { osg::ref_ptr workItem(new PreloadCommonAssetsWorkItem(mResourceSystem)); mSky->listAssetsToPreload(workItem->mModels, workItem->mTextures); mWater->listAssetsToPreload(workItem->mTextures); 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::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(std::move(workItem)); } double RenderingManager::getReferenceTime() const { return mViewer->getFrameStamp()->getReferenceTime(); } SceneUtil::LightManager* RenderingManager::getLightRoot() { return mSceneRoot.get(); } void RenderingManager::setNightEyeFactor(float factor) { if (factor != mNightEyeFactor) { mNightEyeFactor = factor; updateAmbient(); } } void RenderingManager::setAmbientColour(const osg::Vec4f& colour) { mAmbientColor = colour; updateAmbient(); } int RenderingManager::skyGetMasserPhase() const { return mSky->getMasserPhase(); } int RenderingManager::skyGetSecundaPhase() const { return mSky->getSecundaPhase(); } void RenderingManager::skySetMoonColour(bool red) { mSky->setMoonColour(red); } void RenderingManager::configureAmbient(const MWWorld::Cell& cell) { bool isInterior = !cell.isExterior() && !cell.isQuasiExterior(); bool needsAdjusting = false; if (mResourceSystem->getSceneManager()->getLightingMethod() != SceneUtil::LightingMethod::FFP) needsAdjusting = isInterior && !Settings::shaders().mClassicFalloff; osg::Vec4f ambient = SceneUtil::colourFromRGB(cell.getMood().mAmbiantColor); if (needsAdjusting) { constexpr float pR = 0.2126; constexpr float pG = 0.7152; 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(); 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 if (ambient.r() == 0.f && ambient.g() == 0.f && ambient.b() == 0.f) ambient = osg::Vec4( minimumAmbientLuminance, minimumAmbientLuminance, minimumAmbientLuminance, ambient.a()); else ambient *= minimumAmbientLuminance / relativeLuminance; } } setAmbientColour(ambient); 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, float sunVis) { // need to wrap this in a StateUpdater? mSunLight->setDiffuse(diffuse); 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) { osg::Vec3f position = -direction; // The sun is not synchronized with the sunlight because reasons // 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()); // need to wrap this in a StateUpdater? if (Settings::shaders().mMatchSunlightToSun) mSunLight->setPosition(osg::Vec4f(position, 0.f)); else mSunLight->setPosition(osg::Vec4f(-direction, 0.f)); mSky->setSunDirection(position); mPostProcessor->getStateUpdater()->setSunPos(osg::Vec4f(position, 0.f), mNight); } void RenderingManager::addCell(const MWWorld::CellStore* store) { mPathgrid->addCell(store); mWater->changeCell(store); if (store->getCell()->isExterior()) { enableTerrain(true, store->getCell()->getWorldSpace()); mTerrain->loadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } } void RenderingManager::removeCell(const MWWorld::CellStore* store) { mPathgrid->removeCell(store); mActorsPaths->removeCell(store); mObjects->removeCell(store); if (store->getCell()->isExterior()) { getWorldspaceChunkMgr(store->getCell()->getWorldSpace()) .mTerrain->unloadCell(store->getCell()->getGridX(), store->getCell()->getGridY()); } mWater->removeCell(store); } 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); } void RenderingManager::setSkyEnabled(bool enabled) { mSky->setEnabled(enabled); if (enabled) mShadowManager->enableOutdoorMode(); else mShadowManager->enableIndoorMode(Settings::shadows()); mPostProcessor->getStateUpdater()->setIsInterior(!enabled); } bool RenderingManager::toggleBorders() { bool borders = !mTerrain->getBordersVisible(); mTerrain->setBordersVisible(borders); return borders; } bool RenderingManager::toggleRenderMode(RenderMode mode) { if (mode == Render_CollisionDebug || mode == Render_Pathgrid) return mPathgrid->toggleRenderMode(mode); else if (mode == Render_Wireframe) { bool wireframe = !mStateUpdater->getWireframe(); mStateUpdater->setWireframe(wireframe); return wireframe; } else if (mode == Render_Water) { return mWater->toggle(); } else if (mode == Render_Scene) { const auto wm = MWBase::Environment::get().getWindowManager(); unsigned int mask = wm->getCullMask(); bool enabled = !(mask & sToggleWorldMask); if (enabled) mask |= sToggleWorldMask; else mask &= ~sToggleWorldMask; mWater->showWorld(enabled); wm->setCullMask(mask); return enabled; } else if (mode == Render_NavMesh) { return mNavMesh->toggle(); } else if (mode == Render_ActorsPaths) { return mActorsPaths->toggle(); } else if (mode == Render_RecastMesh) { return mRecastMesh->toggle(); } return false; } 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) { mFog->configure(mViewDistance, fogDepth, underwaterFog, dlFactor, dlOffset, color); } SkyManager* RenderingManager::getSkyManager() { return mSky.get(); } void RenderingManager::update(float dt, bool paused) { reportStats(); 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); const MWWorld::Ptr& player = mPlayerAnimation->getPtr(); osg::Vec3f playerPos(player.getRefData().getPosition().asVec3()); float windSpeed = mSky->getBaseWindSpeed(); mSharedUniformStateUpdater->setWindSpeed(windSpeed); mSharedUniformStateUpdater->setPlayerPos(playerPos); } updateNavMesh(); updateRecastMesh(); if (mUpdateProjectionMatrix) { mUpdateProjectionMatrix = false; updateProjectionMatrix(); } mCamera->update(dt, paused); 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); 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) { if (mPlayerAnimation.get()) { setupPlayer(ptr); mPlayerAnimation->updatePtr(ptr); } mCamera->attachTo(ptr); } void RenderingManager::removePlayer(const MWWorld::Ptr& player) { mWater->removeEmitter(player); } void RenderingManager::rotateObject(const MWWorld::Ptr& ptr, const osg::Quat& rot) { if (ptr == mCamera->getTrackingPtr() && !mCamera->isVanityOrPreviewModeEnabled()) { mCamera->rotateCameraToTrackingPtr(); } ptr.getRefData().getBaseNode()->setAttitude(rot); } 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) { ptr.getRefData().getBaseNode()->setScale(scale); if (ptr == mCamera->getTrackingPtr()) // update height of camera mCamera->processViewChange(); } void RenderingManager::removeObject(const MWWorld::Ptr& ptr) { mActorsPaths->remove(ptr); mObjects->removeObject(ptr); mWater->removeEmitter(ptr); } void RenderingManager::setWaterEnabled(bool enabled) { mWater->setEnabled(enabled); mSky->setWaterEnabled(enabled); mPostProcessor->getStateUpdater()->setIsWaterEnabled(enabled); } void RenderingManager::setWaterHeight(float height) { 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) { mScreenshotManager->screenshot(image, w, h); } osg::Vec2f RenderingManager::getScreenCoords(const osg::BoundingBox& bb) { if (bb.valid()) { const osg::Matrix viewProj = mViewer->getCamera()->getViewMatrix() * mViewer->getCamera()->getProjectionMatrix(); const osg::Vec3f worldPoint((bb.xMin() + bb.xMax()) * 0.5f, (bb.yMin() + bb.yMax()) * 0.5f, bb.zMax()); const osg::Vec4f clipPoint = osg::Vec4f(worldPoint, 1.0f) * viewProj; if (clipPoint.w() > 0.f) { const float screenPointX = (clipPoint.x() / clipPoint.w() + 1.f) * 0.5f; const float screenPointY = (clipPoint.y() / clipPoint.w() - 1.f) * (-0.5f); if (screenPointX >= 0.f && screenPointX <= 1.f && screenPointY >= 0.f && screenPointY <= 1.f) return osg::Vec2f(screenPointX, screenPointY); } } return osg::Vec2f(0.5f, 0.f); } 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.mRatio = 0; if (!intersector->containsIntersections()) return result; auto test = [&](const osgUtil::LineSegmentIntersector::Intersection& intersection) { PtrHolder* ptrHolder = nullptr; std::vector refnumMarkers; 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; i < userDataContainer->getNumUserObjects(); ++i) { if (PtrHolder* p = dynamic_cast(userDataContainer->getUserObject(i))) { 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); } } } if (ptrHolder) result.mHitObject = ptrHolder->mPtr; unsigned int vertexCounter = 0; for (unsigned int i = 0; i < refnumMarkers.size(); ++i) { unsigned int intersectionIndex = intersection.indexList.empty() ? 0 : intersection.indexList[0]; if (!refnumMarkers[i]->mNumVertices || (intersectionIndex >= vertexCounter && intersectionIndex < vertexCounter + refnumMarkers[i]->mNumVertices)) { 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); if (result.mHit) { break; } } } return result; } 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 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); if (ignorePlayer) mask &= ~(Mask_Player); if (ignoreActors) 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, std::span ignoreList) { osg::ref_ptr intersector( new osgUtil::LineSegmentIntersector(osgUtil::LineSegmentIntersector::MODEL, origin, dest)); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); mRootNode->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors, ignoreList)); return getIntersectionResult(intersector, mIntersectionVisitor, ignoreList); } 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::Vec3d dist(0.f, 0.f, -maxDistance); dist = dist * mViewer->getCamera()->getProjectionMatrix(); osg::Vec3d end = intersector->getEnd(); end.z() = dist.z(); intersector->setEnd(end); intersector->setIntersectionLimit(osgUtil::LineSegmentIntersector::LIMIT_NEAREST); mViewer->getCamera()->accept(*getIntersectionVisitor(intersector, ignorePlayer, ignoreActors)); return getIntersectionResult(intersector, mIntersectionVisitor); } void RenderingManager::updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated) { mObjects->updatePtr(old, updated); mActorsPaths->updatePtr(old, updated); } void RenderingManager::spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight) { mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX, useAmbientLight); } void RenderingManager::notifyWorldSpaceChanged() { mEffectManager->clear(); mWater->clearRipples(); } void RenderingManager::clear() { mSky->setMoonColour(false); notifyWorldSpaceChanged(); if (mObjectPaging) mObjectPaging->clear(); } MWRender::Animation* RenderingManager::getAnimation(const MWWorld::Ptr& ptr) { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); return mObjects->getAnimation(ptr); } const MWRender::Animation* RenderingManager::getAnimation(const MWWorld::ConstPtr& ptr) const { if (mPlayerAnimation.get() && ptr == mPlayerAnimation->getPtr()) return mPlayerAnimation.get(); return mObjects->getAnimation(ptr); } PostProcessor* RenderingManager::getPostProcessor() { return mPostProcessor; } void RenderingManager::setupPlayer(const MWWorld::Ptr& player) { if (!mPlayerNode) { mPlayerNode = new SceneUtil::PositionAttitudeTransform; mPlayerNode->setNodeMask(Mask_Player); mPlayerNode->setName("Player Root"); mSceneRoot->addChild(mPlayerNode); } mPlayerNode->setUserDataContainer(new osg::DefaultUserDataContainer); mPlayerNode->getUserDataContainer()->addUserObject(new PtrHolder(player)); player.getRefData().setBaseNode(mPlayerNode); mWater->removeEmitter(player); mWater->addEmitter(player); } void RenderingManager::renderPlayer(const MWWorld::Ptr& player) { 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) { NpcAnimation* anim = nullptr; if (ptr == mPlayerAnimation->getPtr()) anim = mPlayerAnimation.get(); else anim = dynamic_cast(mObjects->getAnimation(ptr)); if (anim) { anim->rebuild(); if (mCamera->getTrackingPtr() == ptr) { mCamera->attachTo(ptr); mCamera->setAnimation(anim); } } } void RenderingManager::addWaterRippleEmitter(const MWWorld::Ptr& ptr) { mWater->addEmitter(ptr); } void RenderingManager::removeWaterRippleEmitter(const MWWorld::Ptr& ptr) { mWater->removeEmitter(ptr); } void RenderingManager::emitWaterRipple(const osg::Vec3f& pos) { mWater->emitRipple(pos); } void RenderingManager::updateProjectionMatrix() { 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); 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. float distanceMult = std::cos(osg::DegreesToRadians(std::min(fov, 140.f)) / 2.f); mTerrain->setViewDistance(mViewDistance * (distanceMult ? 1.f / distanceMult : 1.f)); if (mPostProcessor) { 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::general().mTextureMagFilter, Settings::general().mTextureMinFilter, Settings::general().mTextureMipmap, Settings::general().mAnisotropy); mTerrain->updateTextureFiltering(); mWater->processChangedSettings({}); mViewer->startThreading(); } void RenderingManager::updateAmbient() { osg::Vec4f color = mAmbientColor; 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) { 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 && worldspace == ESM::Cell::sDefaultWorldspaceId; 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)); newChunkMgr.mTerrain->enableHeightCullCallback(Settings::terrain().mWaterCulling); 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")) { mTerrain->reportStats(frameNumber, stats); } } void RenderingManager::processChangedSettings(const Settings::CategorySettingVector& changed) { // Only perform a projection matrix update once if a relevant setting is changed. bool updateProjection = false; for (Settings::CategorySettingVector::const_iterator it = changed.begin(); it != changed.end(); ++it) { if (it->first == "Camera" && it->second == "field of view") { mFieldOfView = Settings::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") { setViewDistance(Settings::camera().mViewingDistance); } else if (it->first == "General" && (it->second == "texture filter" || it->second == "texture mipmap" || it->second == "anisotropy")) { updateTextureFiltering(); } else if (it->first == "Water") { mWater->processChangedSettings(changed); } else if (it->first == "Shaders" && it->second == "minimum interior brightness") { if (MWMechanics::getPlayer().isInCell()) configureAmbient(*MWMechanics::getPlayer().getCell()->getCell()); } else if (it->first == "Shaders" && (it->second == "force per pixel lighting" || it->second == "classic falloff")) { 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(Settings::shaders().mMaxLights); auto defines = mResourceSystem->getSceneManager()->getShaderManager().getGlobalDefines(); for (const auto& [name, key] : lightManager->getLightDefines()) defines[name] = key; mResourceSystem->getSceneManager()->getShaderManager().setGlobalDefines(defines); 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(); } } void RenderingManager::setViewDistance(float distance, bool delay) { mViewDistance = distance; if (delay) { mUpdateProjectionMatrix = true; return; } updateProjectionMatrix(); } float RenderingManager::getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace) { return getWorldspaceChunkMgr(worldspace).mTerrain->getHeightAt(pos); } void RenderingManager::overrideFieldOfView(float val) { if (mFieldOfViewOverridden != true || mFieldOfViewOverride != val) { mFieldOfViewOverridden = true; mFieldOfViewOverride = val; updateProjectionMatrix(); } } 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); 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)); const_cast(node.get())->accept(computeBoundsVisitor); osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox(); if (bounds.valid()) { halfExtents[0] = std::abs(bounds.xMax() - bounds.xMin()) / 2.f; halfExtents[1] = std::abs(bounds.yMax() - bounds.yMin()) / 2.f; halfExtents[2] = std::abs(bounds.zMax() - bounds.zMin()) / 2.f; } 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 }); const auto& rotation = ptr.getCellRef().getPosition().rot; if (!ptr.getClass().isActor()) rootNode->setAttitude(osg::Quat(rotation[0], osg::Vec3(-1, 0, 0)) * osg::Quat(rotation[1], osg::Vec3(0, -1, 0)) * osg::Quat(rotation[2], osg::Vec3(0, 0, -1))); 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) { mFieldOfViewOverridden = false; updateProjectionMatrix(); } } void RenderingManager::exportSceneGraph( const MWWorld::Ptr& ptr, const std::filesystem::path& filename, const std::string& format) { osg::Node* node = mViewer->getSceneData(); if (!ptr.isEmpty()) node = ptr.getRefData().getBaseNode(); SceneUtil::writeScene(node, filename, format); } LandManager* RenderingManager::getLandManager() const { return mTerrainStorage->getLandManager(); } void RenderingManager::updateActorPath(const MWWorld::ConstPtr& actor, const std::deque& path, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& start, const osg::Vec3f& end) const { mActorsPaths->update(actor, path, agentBounds, start, end, mNavigator.getSettings()); } void RenderingManager::removeActorPath(const MWWorld::ConstPtr& actor) const { mActorsPaths->remove(actor); } void RenderingManager::setNavMeshNumber(const std::size_t value) { mNavMeshNumber = value; } void RenderingManager::updateNavMesh() { if (!mNavMesh->isEnabled()) return; const auto navMeshes = mNavigator.getNavMeshes(); auto it = navMeshes.begin(); for (std::size_t i = 0; it != navMeshes.end() && i < mNavMeshNumber; ++i) ++it; if (it == navMeshes.end()) { mNavMesh->reset(); } else { try { mNavMesh->update(it->second, mNavMeshNumber, mNavigator.getSettings()); } catch (const std::exception& e) { Log(Debug::Error) << "NavMesh render update exception: " << e.what(); } } } void RenderingManager::updateRecastMesh() { if (!mRecastMesh->isEnabled()) return; mRecastMesh->update(mNavigator.getRecastMeshTiles(), mNavigator.getSettings()); } void RenderingManager::setActiveGrid(const osg::Vec4i& grid) { mTerrain->setActiveGrid(grid); } bool RenderingManager::pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled) { 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)) { mTerrain->rebuildViews(); return true; } return false; } void RenderingManager::pagingBlacklistObject(int type, const MWWorld::ConstPtr& ptr) { if (!ptr.isInCell() || !ptr.getCell()->isExterior() || !mObjectPaging) return; 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() { if (mObjectPaging && mObjectPaging->unlockCache()) { mTerrain->rebuildViews(); return true; } return false; } 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/renderingmanager.hpp000066400000000000000000000257521503074453300245470ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_RENDERINGMANAGER_H #define OPENMW_MWRENDER_RENDERINGMANAGER_H #include "objects.hpp" #include "renderinginterface.hpp" #include "rendermode.hpp" #include #include #include #include #include #include #include #include #include namespace osg { class Group; class PositionAttitudeTransform; } namespace osgUtil { class IntersectionVisitor; class Intersector; } namespace Resource { class ResourceSystem; } namespace osgViewer { class Viewer; } namespace ESM { struct Cell; struct FormId; using RefNum = FormId; } namespace Terrain { class World; } namespace Fallback { class Map; } namespace SceneUtil { class ShadowManager; class WorkQueue; class LightManager; class UnrefQueue; } namespace DetourNavigator { struct Navigator; struct Settings; struct AgentBounds; } namespace MWWorld { class GroundcoverStore; class Cell; } namespace Debug { struct DebugDrawer; } namespace MWRender { class StateUpdater; class SharedUniformStateUpdater; class PerViewUniformStateUpdater; class IntersectionVisitorWithIgnoreList; class EffectManager; class ScreenshotManager; class FogManager; class SkyManager; class NpcAnimation; class Pathgrid; class Camera; class Water; class TerrainStorage; class LandManager; class NavMesh; class ActorsPaths; 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, DetourNavigator::Navigator& navigator, const MWWorld::GroundcoverStore& groundcoverStore, SceneUtil::UnrefQueue& unrefQueue); ~RenderingManager(); osgUtil::IncrementalCompileOperation* getIncrementalCompileOperation(); MWRender::Objects& getObjects() override; Resource::ResourceSystem* getResourceSystem(); SceneUtil::WorkQueue* getWorkQueue(); Terrain::World* getTerrain(); void preloadCommonAssets(); double getReferenceTime() const; SceneUtil::LightManager* getLightRoot(); void setNightEyeFactor(float factor); void setAmbientColour(const osg::Vec4f& colour); int skyGetMasserPhase() const; int skyGetSecundaPhase() const; void skySetMoonColour(bool red); void setSunDirection(const osg::Vec3f& direction); void setSunColour(const osg::Vec4f& diffuse, const osg::Vec4f& specular, float sunVis); void setNight(bool isNight) { mNight = isNight; } 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, ESM::RefId worldspace); void updatePtr(const MWWorld::Ptr& old, const MWWorld::Ptr& updated); void rotateObject(const MWWorld::Ptr& ptr, const osg::Quat& rot); void moveObject(const MWWorld::Ptr& ptr, const osg::Vec3f& pos); void scaleObject(const MWWorld::Ptr& ptr, const osg::Vec3f& scale); void removeObject(const MWWorld::Ptr& ptr); void setWaterEnabled(bool enabled); void setWaterHeight(float level); /// Take a screenshot of w*h onto the given image, not including the GUI. void screenshot(osg::Image* image, int w, int h); struct RayResult { bool mHit; osg::Vec3f mHitNormalWorld; osg::Vec3f mHitPointWorld; MWWorld::Ptr mHitObject; ESM::RefNum mHitRefnum; float mRatio; }; 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); /// Get normalized screen coordinates of the bounding box's summit, where (0,0) is the top left corner osg::Vec2f getScreenCoords(const osg::BoundingBox& bb); void setSkyEnabled(bool enabled); bool toggleRenderMode(RenderMode mode); SkyManager* getSkyManager(); void spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true); /// Clear all savegame-specific data void clear(); /// Clear all worldspace-specific data void notifyWorldSpaceChanged(); void update(float dt, bool paused); 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 removePlayer(const MWWorld::Ptr& player); void setupPlayer(const MWWorld::Ptr& player); void renderPlayer(const MWWorld::Ptr& player); void rebuildPtr(const MWWorld::Ptr& ptr); void processChangedSettings(const Settings::CategorySettingVector& settings); float getNearClipDistance() const { return mNearClip; } float getViewDistance() const { return mViewDistance; } void setViewDistance(float distance, bool delay = false); float getTerrainHeightAt(const osg::Vec3f& pos, ESM::RefId worldspace); // camera stuff Camera* getCamera() { return mCamera.get(); } /// 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; // 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 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); bool pagingEnableObject(int type, const MWWorld::ConstPtr& ptr, bool enabled); void pagingBlacklistObject(int type, const MWWorld::ConstPtr& ptr); bool pagingUnlockCache(); void getPagedRefnums(const osg::Vec4i& activeGrid, std::vector& out); void updateProjectionMatrix(); void setScreenRes(int width, int height); void setNavMeshMode(Settings::NavMeshRenderMode value); private: void updateTextureFiltering(); void updateAmbient(); void setFogColor(const osg::Vec4f& color); 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(); const bool mSkyBlending; 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; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mWorkQueue; osg::ref_ptr mSunLight; DetourNavigator::Navigator& mNavigator; std::unique_ptr mNavMesh; std::size_t mNavMeshNumber = 0; std::unique_ptr mActorsPaths; std::unique_ptr mRecastMesh; std::unique_ptr mPathgrid; std::unique_ptr mObjects; std::unique_ptr mWater; std::unordered_map mWorldspaceChunks; Terrain::World* mTerrain; std::unique_ptr mTerrainStorage; 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; osg::ref_ptr mDebugDraw; osg::ref_ptr mStateUpdater; osg::ref_ptr mSharedUniformStateUpdater; osg::ref_ptr mPerViewUniformStateUpdater; osg::Vec4f mAmbientColor; float mNightEyeFactor; float mNearClip; float mViewDistance; bool mFieldOfViewOverridden; float mFieldOfViewOverride; float mFieldOfView; float mFirstPersonFieldOfView; bool mUpdateProjectionMatrix = false; bool mNight = false; const MWWorld::GroundcoverStore& mGroundCoverStore; void operator=(const RenderingManager&); RenderingManager(const RenderingManager&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/rendermode.hpp000066400000000000000000000005321503074453300233500ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_RENDERMODE_H #define OPENMW_MWRENDER_RENDERMODE_H namespace MWRender { enum RenderMode { Render_CollisionDebug, Render_Wireframe, Render_Pathgrid, Render_Water, Render_Scene, Render_NavMesh, Render_ActorsPaths, Render_RecastMesh, }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/ripples.cpp000066400000000000000000000305451503074453300227040ustar00rootroot00000000000000#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 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(); if (mProgramBlobber != nullptr) { static bool pipelineLogged = [&] { if (mUseCompute) Log(Debug::Info) << "Initialized compute shader pipeline for water ripples"; else Log(Debug::Info) << "Initialized fallback fragment shader pipeline for water ripples"; return true; }(); (void)pipelineLogged; } 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); osg::ref_ptr blobber = shaderManager.getShader("ripples_blobber.frag", defineMap, osg::Shader::FRAGMENT); osg::ref_ptr simulate = shaderManager.getShader("ripples_simulate.frag", defineMap, osg::Shader::FRAGMENT); if (vertex == nullptr || blobber == nullptr || simulate == nullptr) { Log(Debug::Error) << "Failed to load shaders required for fragment shader ripple pipeline"; return; } mProgramBlobber = shaderManager.getProgram(vertex, std::move(blobber)); mProgramSimulation = shaderManager.getProgram(std::move(vertex), std::move(simulate)); } void RipplesSurface::setupComputePipeline() { auto& shaderManager = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr blobber = shaderManager.getShader("core/ripples_blobber.comp", {}, osg::Shader::COMPUTE); osg::ref_ptr simulate = shaderManager.getShader("core/ripples_simulate.comp", {}, osg::Shader::COMPUTE); if (blobber == nullptr || simulate == nullptr) { Log(Debug::Error) << "Failed to load shaders required for compute shader ripple pipeline"; return; } mProgramBlobber = shaderManager.getProgram(nullptr, std::move(blobber)); mProgramSimulation = shaderManager.getProgram(nullptr, std::move(simulate)); } 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 { if (mProgramBlobber == nullptr || mProgramSimulation == nullptr) return; 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 state.pushStateSet(frameState.mStateset); state.apply(); state.applyAttribute(mProgramBlobber); for (const auto& [name, stack] : state.getUniformMap()) { if (!stack.uniformVec.empty()) state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); } 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 state.applyAttribute(mProgramSimulation); for (const auto& [name, stack] : state.getUniformMap()) { if (!stack.uniformVec.empty()) state.getLastAppliedProgramObject()->apply(*(stack.uniformVec.back().first)); } 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); } state.popStateSet(); } 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/ripples.hpp000066400000000000000000000045731503074453300227130ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/ripplesimulation.cpp000066400000000000000000000246551503074453300246330ustar00rootroot00000000000000#include "ripplesimulation.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "vismask.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/actorutil.hpp" namespace { void createWaterRippleStateSet(Resource::ResourceSystem* resourceSystem, osg::Node* node) { int rippleFrameCount = Fallback::Map::getInt("Water_RippleFrameCount"); if (rippleFrameCount <= 0) return; std::string_view tex = Fallback::Map::getString("Water_RippleTexture"); std::vector> textures; for (int i = 0; i < rippleFrameCount; ++i) { std::ostringstream texname; texname << "textures/water/" << tex << std::setw(2) << std::setfill('0') << i << ".dds"; 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::make_shared()); node->addUpdateCallback(controller); 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 SceneUtil::AutoDepth; depth->setWriteMask(false); stateset->setAttributeAndModes(depth, osg::StateAttribute::ON); 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); 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)); mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f, 0.f, 0.f, 0.f)); mat->setColorMode(osg::Material::DIFFUSE); stateset->setAttributeAndModes(mat, osg::StateAttribute::ON); 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) , 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)); 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); mParticleNode = new osg::PositionAttitudeTransform; mParticleNode->setName("Ripple Root"); mParticleNode->addChild(updater); mParticleNode->addChild(mParticleSystem); mParticleNode->setNodeMask(Mask_Water); createWaterRippleStateSet(resourceSystem, mParticleNode); resourceSystem->getSceneManager()->recreateShaders(mParticleNode); mParent->addChild(mParticleNode); } RippleSimulation::~RippleSimulation() { mParent->removeChild(mParticleNode); } void RippleSimulation::update(float dt) { const MWBase::World* world = MWBase::Environment::get().getWorld(); for (Emitter& emitter : mEmitters) { 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); } } } 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); } void RippleSimulation::removeEmitter(const MWWorld::ConstPtr& ptr) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { if (it->mPtr == ptr) { mEmitters.erase(it); return; } } } void RippleSimulation::updateEmitterPtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& ptr) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end(); ++it) { if (it->mPtr == old) { it->mPtr = ptr; return; } } } void RippleSimulation::removeCell(const MWWorld::CellStore* store) { for (std::vector::iterator it = mEmitters.begin(); it != mEmitters.end();) { if ((it->mPtr.isInCell() && it->mPtr.getCell() == store) && it->mPtr != MWMechanics::getPlayer()) { it = mEmitters.erase(it); } else ++it; } } void RippleSimulation::emitRipple(const osg::Vec3f& pos) { if (std::abs(pos.z() - mParticleNode->getPosition().z()) < 20) { 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)); } } } void RippleSimulation::setWaterHeight(float height) { mParticleNode->setPosition(osg::Vec3f(0, 0, height)); } void RippleSimulation::clear() { for (int i = 0; i < mParticleSystem->numParticles(); ++i) mParticleSystem->destroyParticle(i); } } openmw-openmw-0.49.0/apps/openmw/mwrender/ripplesimulation.hpp000066400000000000000000000034211503074453300246240ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_RIPPLESIMULATION_H #define OPENMW_MWRENDER_RIPPLESIMULATION_H #include #include "../mwworld/ptr.hpp" #include "ripples.hpp" namespace osg { class Group; class PositionAttitudeTransform; } namespace osgParticle { class ParticleSystem; } namespace Resource { class ResourceSystem; } namespace Fallback { class Map; } namespace MWRender { struct Emitter { MWWorld::ConstPtr mPtr; osg::Vec3f mLastEmitPosition; float mScale; float mForce; float mTimer; }; class RippleSimulation { public: RippleSimulation(osg::Group* parent, Resource::ResourceSystem* resourceSystem); ~RippleSimulation(); /// @param dt Time since the last frame 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 removeCell(const MWWorld::CellStore* store); void emitRipple(const osg::Vec3f& pos); /// Change the height of the water surface, thus moving all ripples with it void setWaterHeight(float height); /// Remove all active ripples void clear(); void setRipples(Ripples* ripples) { mRipples = ripples; } private: osg::ref_ptr mParent; osg::ref_ptr mParticleSystem; osg::ref_ptr mParticleNode; std::vector mEmitters; Ripples* mRipples = nullptr; int mMaxNumberRipples; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/rotatecontroller.cpp000066400000000000000000000033751503074453300246310ustar00rootroot00000000000000#include "rotatecontroller.hpp" #include #include namespace MWRender { RotateController::RotateController(osg::Node* relativeTo) : mEnabled(true) , mRelativeTo(relativeTo) { } void RotateController::setEnabled(bool enabled) { mEnabled = enabled; } void RotateController::setRotate(const osg::Quat& rotate) { mRotate = rotate; } void RotateController::setOffset(const osg::Vec3f& offset) { mOffset = offset; } void RotateController::operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv) { if (!mEnabled) { traverse(node, nv); return; } osg::Matrix matrix = node->getMatrix(); osg::Quat worldOrient; osg::NodePathList nodepaths = node->getParentalNodePaths(mRelativeTo); if (!nodepaths.empty()) { osg::Matrixf worldMat = osg::computeLocalToWorld(nodepaths[0]); worldOrient = worldMat.getRotate(); } 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/rotatecontroller.hpp000066400000000000000000000022451503074453300246310ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_ROTATECONTROLLER_H #define OPENMW_MWRENDER_ROTATECONTROLLER_H #include #include namespace osg { class MatrixTransform; } namespace MWRender { /// 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); const osg::Vec3f& getOffset() const { return mOffset; } const osg::Quat& getRotate() const { return mRotate; } osg::Node* getRelativeTo() const { return mRelativeTo; } void operator()(osg::MatrixTransform* node, osg::NodeVisitor* nv); protected: bool mEnabled; osg::Vec3f mOffset; osg::Quat mRotate; osg::Node* mRelativeTo; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/screenshotmanager.cpp000066400000000000000000000102101503074453300247210ustar00rootroot00000000000000#include "screenshotmanager.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "postprocessor.hpp" namespace MWRender { class NotifyDrawCompletedCallback : public osg::Camera::DrawCallback { public: NotifyDrawCompletedCallback() : mDone(false) , mFrame(0) { } void operator()(osg::RenderInfo& renderInfo) const override { std::lock_guard lock(mMutex); if (renderInfo.getState()->getFrameStamp()->getFrameNumber() >= mFrame && !mDone) { mDone = true; mCondition.notify_one(); } } void waitTillDone() { std::unique_lock lock(mMutex); if (mDone) return; mCondition.wait(lock); } void reset(unsigned int frame) { std::lock_guard lock(mMutex); mDone = false; mFrame = frame; } mutable std::condition_variable mCondition; mutable std::mutex mMutex; mutable bool mDone; unsigned int mFrame; }; class ReadImageFromFramebufferCallback : public osg::Drawable::DrawCallback { public: ReadImageFromFramebufferCallback(osg::Image* image, int width, int height) : mWidth(width) , mHeight(height) , mImage(image) { } void drawImplementation(osg::RenderInfo& renderInfo, const osg::Drawable* /*drawable*/) const override { int screenW = renderInfo.getCurrentCamera()->getViewport()->width(); int screenH = renderInfo.getCurrentCamera()->getViewport()->height(); 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; 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) : mViewer(viewer) , mDrawCompleteCallback(new NotifyDrawCompletedCallback) { } ScreenshotManager::~ScreenshotManager() {} void ScreenshotManager::screenshot(osg::Image* image, int w, int h) { 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 camera->addChild(tempDrw); // Ref https://gitlab.com/OpenMW/openmw/-/issues/6013 mDrawCompleteCallback->reset(mViewer->getFrameStamp()->getFrameNumber()); mViewer->getCamera()->setFinalDrawCallback(mDrawCompleteCallback); mViewer->eventTraversal(); mViewer->updateTraversal(); mViewer->renderingTraversals(); mDrawCompleteCallback->waitTillDone(); // 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/screenshotmanager.hpp000066400000000000000000000010231503074453300247300ustar00rootroot00000000000000#ifndef MWRENDER_SCREENSHOTMANAGER_H #define MWRENDER_SCREENSHOTMANAGER_H #include #include namespace MWRender { class NotifyDrawCompletedCallback; class ScreenshotManager { public: ScreenshotManager(osgViewer::Viewer* viewer); ~ScreenshotManager(); void screenshot(osg::Image* image, int w, int h); private: osg::ref_ptr mViewer; osg::ref_ptr mDrawCompleteCallback; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/sky.cpp000066400000000000000000001053441503074453300220340ustar00rootroot00000000000000#include "sky.hpp" #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 "renderbin.hpp" #include "skyutil.hpp" #include "util.hpp" #include "vismask.hpp" namespace { class WrapAroundOperator : public osgParticle::Operator { public: WrapAroundOperator(osg::Camera* camera, const osg::Vec3& wrapRange) : osgParticle::Operator() , mCamera(camera) , mWrapRange(wrapRange) , mHalfWrapRange(mWrapRange / 2.0) { mPreviousCameraPosition = getCameraPosition(); } osg::Object* cloneType() const override { return nullptr; } osg::Object* clone(const osg::CopyOp& op) const override { return nullptr; } void operate(osgParticle::Particle* P, double dt) override {} void operateParticles(osgParticle::ParticleSystem* ps, double dt) override { osg::Vec3 position = getCameraPosition(); osg::Vec3 positionDifference = position - mPreviousCameraPosition; osg::Matrix toWorld, toLocal; std::vector worldMatrices = ps->getWorldMatrices(); if (!worldMatrices.empty()) { toWorld = worldMatrices[0]; toLocal.invert(toWorld); } 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); for (int j = 0; j < 3; ++j) // wrap-around in all 3 dimensions { osg::Vec3 pos = p->getPosition(); 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]; p->setPosition(pos); } p->setPosition(toLocal.preMult(p->getPosition())); } mPreviousCameraPosition = position; } protected: osg::Camera* mCamera; osg::Vec3 mPreviousCameraPosition; osg::Vec3 mWrapRange; osg::Vec3 mHalfWrapRange; osg::Vec3 getCameraPosition() { return mCamera->getInverseViewMatrix().getTrans(); } }; class WeatherAlphaOperator : public osgParticle::Operator { public: WeatherAlphaOperator(float& alpha, bool rain) : mAlpha(alpha) , mIsRain(rain) { } osg::Object* cloneType() const override { return nullptr; } osg::Object* clone(const osg::CopyOp& op) const override { return nullptr; } void operate(osgParticle::Particle* particle, double dt) override { constexpr float rainThreshold = 0.6f; // Rain_Threshold? float alpha = mIsRain ? mAlpha * rainThreshold : mAlpha; particle->setAlphaRange(osgParticle::rangef(alpha, alpha)); } private: float& mAlpha; bool mIsRain; }; // Updater for alpha value on a node's StateSet. Assumes the node has an existing Material StateAttribute. class AlphaFader : public SceneUtil::StateSetUpdater { public: /// @param alpha the variable alpha value is recovered from AlphaFader(const float& alpha) : mAlpha(alpha) { } void setDefaults(osg::StateSet* stateset) override { // need to create a deep copy of StateAttributes we will modify osg::Material* mat = static_cast(stateset->getAttribute(osg::StateAttribute::MATERIAL)); stateset->setAttribute(osg::clone(mat, osg::CopyOp::DEEP_COPY_ALL), osg::StateAttribute::ON); } void 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(const float& alpha) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mAlpha(alpha) { } void apply(osg::Node& node) override { if (osg::StateSet* stateset = node.getStateSet()) { if (stateset->getAttribute(osg::StateAttribute::MATERIAL)) { SceneUtil::CompositeStateSetUpdater* composite = nullptr; osg::Callback* callback = node.getUpdateCallback(); while (callback) { composite = dynamic_cast(callback); if (composite) break; callback = callback->getNestedCallback(); } osg::ref_ptr alphaFader = new AlphaFader(mAlpha); if (composite) composite->addController(alphaFader); else node.addUpdateCallback(alphaFader); } } traverse(node); } private: const 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; }; } namespace MWRender { 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) , mTimescaleClouds(Fallback::Map::getBool("Weather_Timescale_Clouds")) , mCloudAnimationTimer(0.f) , mStormParticleDirection(MWWorld::Weather::defaultDirection()) , mStormDirection(MWWorld::Weather::defaultDirection()) , mClouds() , mNextClouds() , mCloudBlendFactor(0.f) , mCloudSpeed(0.f) , mStarsOpacity(0.f) , 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) , mSunglareEnabled(true) , mPrecipitationAlpha(0.f) , mDirtyParticlesEffect(false) { mSkyRootNode = new CameraRelativeTransform; mSkyRootNode->setName("Sky Root"); // Assign empty program to specify we don't want shaders when we are rendering in FFP pipeline if (!mSceneManager->getForceShaders()) mSkyRootNode->getOrCreateStateSet()->setAttributeAndModes(new osg::Program(), osg::StateAttribute::OVERRIDE | osg::StateAttribute::PROTECTED | osg::StateAttribute::ON); mSceneManager->setUpNormalsRTForStateSet(mSkyRootNode->getOrCreateStateSet(), false); SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*mSkyRootNode->getOrCreateStateSet()); parentNode->addChild(mSkyRootNode); 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); mSkyRootNode->addChild(mSkyRTT); } mSkyNode = new osg::Group; mSkyNode->setNodeMask(Mask_Sky); mSkyNode->addChild(mEarlyRenderBinRoot); mSkyRootNode->addChild(mSkyNode); mUnderwaterSwitch = new UnderwaterSwitchCallback(mSkyRootNode); mPrecipitationOcclusion = Settings::shaders().mWeatherParticleOcclusion; mPrecipitationOccluder = std::make_unique(mSkyRootNode, parentNode, rootNode, camera); } void SkyManager::create() { assert(!mCreated); bool forceShaders = mSceneManager->getForceShaders(); mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere.get(), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); mAtmosphereDay->accept(modAtmosphere); mAtmosphereUpdater = new AtmosphereUpdater; mAtmosphereDay->addUpdateCallback(mAtmosphereUpdater); mAtmosphereNightNode = new osg::PositionAttitudeTransform; mAtmosphereNightNode->setNodeMask(0); mEarlyRenderBinRoot->addChild(mAtmosphereNightNode); osg::ref_ptr atmosphereNight; if (mSceneManager->getVFS()->exists(Settings::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); ModVertexAlphaVisitor modStars(ModVertexAlphaVisitor::Stars); atmosphereNight->accept(modStars); mAtmosphereNightUpdater = new AtmosphereNightUpdater(mSceneManager->getImageManager(), forceShaders); atmosphereNight->addUpdateCallback(mAtmosphereNightUpdater); 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); 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); } osg::ref_ptr depth = new SceneUtil::AutoDepth; depth->setWriteMask(false); mEarlyRenderBinRoot->getOrCreateStateSet()->setAttributeAndModes(depth); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_BLEND, osg::StateAttribute::ON); mEarlyRenderBinRoot->getOrCreateStateSet()->setMode(GL_FOG, osg::StateAttribute::OFF); mMoonScriptColor = Fallback::Map::getColour("Moons_Script_Color"); mCreated = true; } void 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); mSkyNode->addChild(mRainNode); if (mPrecipitationOcclusion) mPrecipitationOccluder->enable(); } void SkyManager::destroyRain() { if (!mRainNode) return; mSkyNode->removeChild(mRainNode); mRainNode = nullptr; mPlacer = nullptr; mCounter = nullptr; mRainParticleSystem = nullptr; mRainShooter = nullptr; mPrecipitationOccluder->disable(); } SkyManager::~SkyManager() { if (mSkyRootNode) { mSkyRootNode->getParent(0)->removeChild(mSkyRootNode); mSkyRootNode = nullptr; } } int SkyManager::getMasserPhase() const { if (!mCreated) return 0; return mMasser->getPhaseInt(); } int SkyManager::getSecundaPhase() const { if (!mCreated) return 0; return mSecunda->getPhaseInt(); } bool SkyManager::isEnabled() { return mEnabled; } bool SkyManager::hasRain() const { return mRainNode != nullptr; } bool SkyManager::getRainRipplesEnabled() const { if (!mEnabled || mIsStorm) return false; if (hasRain()) return mRainRipplesEnabled; if (mParticleNode && mCurrentParticleEffect == Settings::models().mWeathersnow.get()) return mSnowRipplesEnabled; return false; } float SkyManager::getPrecipitationAlpha() const { if (mEnabled && !mIsStorm && (hasRain() || mParticleNode)) return mPrecipitationAlpha; return 0.f; } void SkyManager::update(float duration) { if (!mEnabled) return; 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::models().mWeatherblizzard.get()) quat.makeRotate(osg::Vec3f(-1, 0, 0), mStormParticleDirection); mParticleNode->setAttitude(quat); } const float timeScale = MWBase::Environment::get().getWorld()->getTimeManager()->getGameTimeScale(); // UV Scroll the clouds float cloudDelta = duration * mCloudSpeed / 400.f; if (mTimescaleClouds) cloudDelta *= timeScale / 60.f; mCloudAnimationTimer += cloudDelta; if (mCloudAnimationTimer >= 4.f) mCloudAnimationTimer -= 4.f; mNextCloudUpdater->setTextureCoord(mCloudAnimationTimer); mCloudUpdater->setTextureCoord(mCloudAnimationTimer); // morrowind rotates each cloud mesh independently osg::Quat rotation; rotation.makeRotate(MWWorld::Weather::defaultDirection(), mStormDirection); mCloudMesh->setAttitude(rotation); if (mNextCloudMesh->getNodeMask()) { rotation.makeRotate(MWWorld::Weather::defaultDirection(), mNextStormDirection); mNextCloudMesh->setAttitude(rotation); } // 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::setEnabled(bool enabled) { if (enabled && !mCreated) create(); const osg::Node::NodeMask mask = enabled ? Mask_Sky : 0u; mEarlyRenderBinRoot->setNodeMask(mask); mSkyNode->setNodeMask(mask); if (!enabled && mParticleNode && mParticleEffect) { mCurrentParticleEffect.clear(); mDirtyParticlesEffect = true; } 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) { 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); } } void SkyManager::switchUnderwaterRain() { if (!mRainParticleSystem) return; bool freeze = mUnderwaterSwitch->isUnderwater(); mRainParticleSystem->setFrozen(freeze); } 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()) { createRain(); } else { destroyRain(); } } updateRainParameters(); mIsStorm = weather.mIsStorm; if (mIsStorm) mStormDirection = weather.mStormDirection; if (mDirtyParticlesEffect || (mCurrentParticleEffect != weather.mParticleEffect)) { mDirtyParticlesEffect = false; mCurrentParticleEffect = weather.mParticleEffect; // cleanup old particles if (mParticleEffect) { mParticleNode->removeChild(mParticleEffect); mParticleEffect = nullptr; } if (mCurrentParticleEffect.empty()) { if (mParticleNode) { mSkyNode->removeChild(mParticleNode); mParticleNode = nullptr; } if (mRainEffect.empty()) { mPrecipitationOccluder->disable(); } } else { if (!mParticleNode) { mParticleNode = new osg::PositionAttitudeTransform; mParticleNode->addCullCallback(mUnderwaterSwitch); mParticleNode->setNodeMask(Mask_WeatherParticles); mSkyNode->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); } } } if (mClouds != weather.mCloudTexture) { mClouds = weather.mCloudTexture; const VFS::Path::Normalized texture = Misc::ResourceHelpers::correctTexturePath(mClouds, mSceneManager->getVFS()); osg::ref_ptr cloudTex = new osg::Texture2D(mSceneManager->getImageManager()->getImage(texture)); cloudTex->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); cloudTex->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); mCloudUpdater->setTexture(std::move(cloudTex)); } if (mStormDirection != weather.mStormDirection) mStormDirection = weather.mStormDirection; if (mNextStormDirection != weather.mNextStormDirection) mNextStormDirection = weather.mNextStormDirection; if (mNextClouds != weather.mNextCloudTexture) { mNextClouds = weather.mNextCloudTexture; if (!mNextClouds.empty()) { 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 (mCloudBlendFactor != weather.mCloudBlendFactor) { mCloudBlendFactor = std::clamp(weather.mCloudBlendFactor, 0.f, 1.f); mCloudUpdater->setOpacity(1.f - mCloudBlendFactor); mNextCloudUpdater->setOpacity(mCloudBlendFactor); mNextCloudMesh->setNodeMask(mCloudBlendFactor > 0.f ? ~0u : 0); } if (mCloudColour != weather.mFogColor) { osg::Vec4f clr(weather.mFogColor); clr += osg::Vec4f(0.13f, 0.13f, 0.13f, 0.f); mCloudUpdater->setEmissionColor(clr); mNextCloudUpdater->setEmissionColor(clr); mCloudColour = weather.mFogColor; } if (mSkyColour != weather.mSkyColor) { mSkyColour = weather.mSkyColor; mAtmosphereUpdater->setEmissionColor(mSkyColour); mMasser->setAtmosphereColor(mSkyColour); mSecunda->setAtmosphereColor(mSkyColour); } if (mFogColour != weather.mFogColor) { mFogColour = weather.mFogColor; } mCloudSpeed = weather.mCloudSpeed; mMasser->adjustTransparency(weather.mGlareView); mSecunda->adjustTransparency(weather.mGlareView); mSun->setColor(weather.mSunDiscColor); mSun->adjustTransparency(weather.mGlareView * weather.mSunDiscColor.a()); float nextStarsOpacity = weather.mNightFade * weather.mGlareView; if (weather.mNight && mStarsOpacity != nextStarsOpacity) { mStarsOpacity = nextStarsOpacity; mAtmosphereNightUpdater->setFade(mStarsOpacity); } mAtmosphereNightNode->setNodeMask(weather.mNight ? ~0u : 0); mPrecipitationAlpha = weather.mPrecipitationAlpha; } float SkyManager::getBaseWindSpeed() const { if (!mCreated) return 0.f; return mBaseWindSpeed; } void SkyManager::setSunglare(bool enabled) { mSunglareEnabled = enabled; if (mSun) mSun->setSunglare(mSunglareEnabled); } void SkyManager::sunEnable() { if (!mCreated) return; mSun->setVisible(true); } void SkyManager::sunDisable() { if (!mCreated) return; mSun->setVisible(false); } void SkyManager::setStormParticleDirection(const osg::Vec3f& direction) { mStormParticleDirection = direction; } void SkyManager::setSunDirection(const osg::Vec3f& direction) { if (!mCreated) return; mSun->setDirection(direction); } void SkyManager::setMasserState(const MoonState& state) { if (!mCreated) return; mMasser->setState(state); } void SkyManager::setSecundaState(const MoonState& state) { if (!mCreated) return; mSecunda->setState(state); } void SkyManager::setGlareTimeOfDayFade(float val) { mSun->setGlareTimeOfDayFade(val); } void SkyManager::setWaterHeight(float height) { mUnderwaterSwitch->setWaterLevel(height); } 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); models.push_back(Settings::models().mWeatherashcloud); models.push_back(Settings::models().mWeatherblightcloud); models.push_back(Settings::models().mWeathersnow); models.push_back(Settings::models().mWeatherblizzard); 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"); } void SkyManager::setWaterEnabled(bool enabled) { mUnderwaterSwitch->setEnabled(enabled); } } openmw-openmw-0.49.0/apps/openmw/mwrender/sky.hpp000066400000000000000000000130501503074453300220310ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_SKY_H #define OPENMW_MWRENDER_SKY_H #include #include #include #include #include #include #include "precipitationocclusion.hpp" #include "skyutil.hpp" namespace osg { class Group; class Node; class Material; class PositionAttitudeTransform; class Camera; } namespace osgParticle { class ParticleSystem; class BoxPlacer; } namespace Resource { class SceneManager; } namespace SceneUtil { class RTTNode; } 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, osg::Group* rootNode, osg::Camera* camera, Resource::SceneManager* sceneManager, bool enableSkyRTT); ~SkyManager(); void update(float duration); void setEnabled(bool enabled); int getMasserPhase() const; ///< 0 new moon, 1 waxing or waning cresecent, 2 waxing or waning half, /// 3 waxing or waning gibbous, 4 full moon int getSecundaPhase() const; ///< 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); ///< change Secunda colour to red void setWeather(const WeatherResult& weather); void sunEnable(); void sunDisable(); bool isEnabled(); bool hasRain() const; bool getRainRipplesEnabled() const; float getPrecipitationAlpha() const; void setStormParticleDirection(const osg::Vec3f& direction); void setSunDirection(const osg::Vec3f& direction); void setMasserState(const MoonState& state); void setSecundaState(const MoonState& state); void setGlareTimeOfDayFade(float val); /// Enable or disable the water plane (used to remove underwater weather particles) void setWaterEnabled(bool enabled); /// Set height of water plane (used to remove underwater weather particles) void setWaterHeight(float height); 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() void createRain(); void destroyRain(); void switchUnderwaterRain(); void updateRainParameters(); Resource::SceneManager* mSceneManager; osg::Camera* mCamera; osg::ref_ptr mSkyRootNode; osg::ref_ptr mSkyNode; osg::ref_ptr mEarlyRenderBinRoot; osg::ref_ptr mParticleNode; osg::ref_ptr mParticleEffect; osg::ref_ptr mUnderwaterSwitch; osg::ref_ptr mCloudNode; osg::ref_ptr mCloudUpdater; osg::ref_ptr mNextCloudUpdater; osg::ref_ptr mCloudMesh; osg::ref_ptr mNextCloudMesh; osg::ref_ptr mAtmosphereDay; osg::ref_ptr mAtmosphereNightNode; float mAtmosphereNightRoll; osg::ref_ptr mAtmosphereNightUpdater; osg::ref_ptr mAtmosphereUpdater; std::unique_ptr mSun; std::unique_ptr mMasser; std::unique_ptr mSecunda; osg::ref_ptr mRainNode; osg::ref_ptr mRainParticleSystem; osg::ref_ptr mPlacer; osg::ref_ptr mCounter; osg::ref_ptr mRainShooter; bool mPrecipitationOcclusion = false; std::unique_ptr mPrecipitationOccluder; bool mCreated; bool mIsStorm; bool mTimescaleClouds; float mCloudAnimationTimer; // 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; std::string mNextClouds; float mCloudBlendFactor; float mCloudSpeed; float mStarsOpacity; osg::Vec4f mCloudColour; osg::Vec4f mSkyColour; osg::Vec4f mFogColour; VFS::Path::Normalized mCurrentParticleEffect; std::string mRainEffect; float mRainSpeed; float mRainDiameter; float mRainMinHeight; float mRainMaxHeight; float mRainEntranceSpeed; int mRainMaxRaindrops; bool mRainRipplesEnabled; bool mSnowRipplesEnabled; float mWindSpeed; float mBaseWindSpeed; bool mEnabled; bool mSunglareEnabled; float mPrecipitationAlpha; bool mDirtyParticlesEffect; osg::Vec4f mMoonScriptColor; osg::ref_ptr mSkyRTT; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/skyutil.cpp000066400000000000000000001326371503074453300227370ustar00rootroot00000000000000#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; // 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->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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/skyutil.hpp000066400000000000000000000224251503074453300227350ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/terrainstorage.cpp000066400000000000000000000076311503074453300242570ustar00rootroot00000000000000#include "terrainstorage.hpp" #include #include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include "landmanager.hpp" namespace MWRender { 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()); } TerrainStorage::~TerrainStorage() { mResourceSystem->removeResourceManager(mLandManager.get()); } bool TerrainStorage::hasData(ESM::ExteriorCellLocation cellLocation) { 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; return esmStore.get().search(cellLocation) != nullptr; } else { return esmStore.get().search(cellLocation.mX, cellLocation.mY) != nullptr; } } static void BoundUnion(float& minX, float& maxX, float& minY, float& maxY, float x, float y) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } 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(); if (ESM::isEsm4Ext(worldspace)) { 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 { return mLandManager.get(); } osg::ref_ptr TerrainStorage::getLand(ESM::ExteriorCellLocation cellLocation) { return mLandManager->getLand(cellLocation); } const std::string* TerrainStorage::getLandTexture(std::uint16_t index, int plugin) { const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore(); return esmStore.get().search(index, plugin); } } openmw-openmw-0.49.0/apps/openmw/mwrender/terrainstorage.hpp000066400000000000000000000024371503074453300242630ustar00rootroot00000000000000#ifndef MWRENDER_TERRAINSTORAGE_H #define MWRENDER_TERRAINSTORAGE_H #include #include #include namespace MWRender { class LandManager; /// @brief Connects the ESM Store used in OpenMW with the ESMTerrain storage. class TerrainStorage : public ESMTerrain::Storage { public: 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(ESM::ExteriorCellLocation cellLocation) override; const std::string* getLandTexture(std::uint16_t index, int plugin) 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, ESM::RefId worldspace) override; LandManager* getLandManager() const; private: std::unique_ptr mLandManager; Resource::ResourceSystem* mResourceSystem; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/transparentpass.cpp000066400000000000000000000132571503074453300244570ustar00rootroot00000000000000#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"); } } openmw-openmw-0.49.0/apps/openmw/mwrender/transparentpass.hpp000066400000000000000000000020641503074453300244560ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwrender/util.cpp000066400000000000000000000061751503074453300222050ustar00rootroot00000000000000#include "util.hpp" #include #include #include #include #include #include #include #include namespace MWRender { namespace { 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; if (node.getUserValue("overrideFx", index)) { if (index == 1) overrideTexture(mTexture, mResourcesystem, node); } traverse(node); } std::string_view mTexture; Resource::ResourceSystem* mResourcesystem; }; } void overrideFirstRootTexture(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& node) { TextureOverrideVisitor overrideVisitor(texture, resourceSystem); node.accept(overrideVisitor); } 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 (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, new SceneUtil::TextureType("diffuseMap"), osg::StateAttribute::OVERRIDE); node.setStateSet(stateset); } bool shouldAddMSAAIntermediateTarget() { return Settings::shaders().mAntialiasAlphaTest && Settings::video().mAntialiasing > 1; } osg::ref_ptr makeVFXLightModelInstance() { osg::ref_ptr lightModel = new osg::LightModel; lightModel->setAmbientIntensity({ 1, 1, 1, 1 }); return lightModel; } const osg::ref_ptr& getVFXLightModelInstance() { static const osg::ref_ptr lightModel = makeVFXLightModelInstance(); return lightModel; } } openmw-openmw-0.49.0/apps/openmw/mwrender/util.hpp000066400000000000000000000021331503074453300222000ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_UTIL_H #define OPENMW_MWRENDER_UTIL_H #include #include #include namespace osg { class Node; } namespace Resource { class ResourceSystem; } 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(std::string_view texture, Resource::ResourceSystem* resourceSystem, osg::Node& 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 { public: void operator()(osg::Node* node, osg::NodeVisitor* nv) override { // no traverse() } }; bool shouldAddMSAAIntermediateTarget(); const osg::ref_ptr& getVFXLightModelInstance(); } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/vismask.hpp000066400000000000000000000055321503074453300227060ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_VISMASK_H #define OPENMW_MWRENDER_VISMASK_H namespace MWRender { /// Node masks used for controlling visibility of game objects. /// @par Any node in the OSG scene graph can have a node mask. When traversing the scene graph, /// the node visitor's traversal mask is bitwise AND'ed with the node mask. If the result of this test is /// 0, then the node and all its child nodes are not processed. /// @par Important traversal masks are the camera's cull mask (determines what is visible), /// the update visitor mask (what is updated) and the intersection visitor mask (what is /// selectable through mouse clicks or other intersection tests). /// @par In practice, it can be useful to make a "hierarchy" out of the node masks - e.g. in OpenMW, /// all 3D rendering nodes are child of a Scene Root node with Mask_Scene. When we do not want 3D rendering, /// we can just omit Mask_Scene from the traversal mask, and do not need to omit all the individual /// element masks (water, sky, terrain, etc.) since the traversal will already have stopped at the Scene root node. /// @par The comments within the VisMask enum should give some hints as to what masks are commonly "child" of /// another mask, or what type of node this mask is usually set on. /// @note The mask values are not serialized within models, nor used in any other way that would break backwards /// compatibility if the enumeration values were to be changed. Feel free to change them when it makes sense. enum VisMask : unsigned int { 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), // child of Sky Mask_Sun = (1 << 12), Mask_WeatherParticles = (1 << 13), // top level masks Mask_Scene = (1 << 14), Mask_GUI = (1 << 15), // Set on a ParticleSystem Drawable Mask_ParticleSystem = (1 << 16), // Set on cameras within the main scene graph Mask_RenderToTexture = (1 << 17), Mask_PreCompile = (1 << 18), // Set on a camera's cull mask to enable the LightManager Mask_Lighting = (1 << 19), 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 openmw-openmw-0.49.0/apps/openmw/mwrender/water.cpp000066400000000000000000000776041503074453300223570ustar00rootroot00000000000000#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 "../mwworld/cellstore.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 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(); 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 { public: FlipCallback(const osg::Plane* cullPlane) : mCullPlane(cullPlane) { } 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; }; public: ClipCullNode() { addCullCallback(new PlaneCullCallback(&mPlane)); mClipNodeTransform = new osg::Group; mClipNodeTransform->addCullCallback(new FlipCallback(&mPlane)); osg::Group::addChild(mClipNodeTransform); mClipNode = new osg::ClipNode; mClipNodeTransform->addChild(mClipNode); } 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: osg::ref_ptr mClipNodeTransform; osg::ref_ptr mClipNode; osg::Plane mPlane; }; /// 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: 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); } }; /// 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) { 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()); if (cv->getEyeLocal().z() > 0) modelViewMatrix->preMultTranslate(osg::Vec3f(0, 0, -diff)); else modelViewMatrix->preMultTranslate(osg::Vec3f(0, 0, diff)); cv->pushModelViewMatrix(modelViewMatrix, osg::Transform::RELATIVE_RF); traverse(node, cv); cv->popModelViewMatrix(); } else traverse(node, cv); } }; class RainSettingsUpdater : public SceneUtil::StateSetUpdater { public: RainSettingsUpdater() : mRainIntensity(0.f) , mEnableRipples(false) { } 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: float mRainIntensity; bool mEnableRipples; }; class Refraction : public SceneUtil::RTTNode { public: Refraction(uint32_t rttSize) : RTTNode(rttSize, rttSize, 0, false, 1, StereoAwareness::Aware, shouldAddMSAAIntermediateTarget()) , mNodeMask(Refraction::sDefaultCullMask) { setDepthBufferInternalFormat(GL_DEPTH24_STENCIL8); mClipCullNode = new ClipCullNode; } 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()); } void apply(osg::Camera* camera) override { camera->setViewMatrix(mViewMatrix); camera->setCullMask(mNodeMask); } void setScene(osg::Node* scene) { if (mScene) mClipCullNode->removeChild(mScene); mScene = scene; mClipCullNode->addChild(scene); } void setWaterLevel(float waterLevel) { const float refractionScale = Settings::water().mRefractionScale; mViewMatrix = osg::Matrix::scale(1, 1, refractionScale) * osg::Matrix::translate(0, 0, (1.0 - refractionScale) * waterLevel); mClipCullNode->setPlane(osg::Plane(osg::Vec3d(0, 0, -1), osg::Vec3d(0, 0, waterLevel))); } void showWorld(bool show) { if (show) mNodeMask = Refraction::sDefaultCullMask; else mNodeMask = Refraction::sDefaultCullMask & ~sToggleWorldMask; } private: osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; unsigned int mNodeMask; static constexpr unsigned int sDefaultCullMask = Mask_Effect | Mask_Scene | Mask_Object | Mask_Static | Mask_Terrain | Mask_Actor | Mask_ParticleSystem | Mask_Sky | Mask_Sun | Mask_Player | Mask_Lighting | Mask_Groundcover; }; class Reflection : public SceneUtil::RTTNode { 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; } void setDefaults(osg::Camera* camera) override { camera->setReferenceFrame(osg::Camera::RELATIVE_RF); camera->setSmallFeatureCullingPixelSize(Settings::water().mSmallFeatureCullingPixelSize); camera->setName("ReflectionCamera"); camera->addCullCallback(new InheritViewPointCallback); // Inform the shader that we're in a reflection camera->getOrCreateStateSet()->addUniform(new osg::Uniform("isReflection", true)); // 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); camera->addChild(mClipCullNode); camera->setNodeMask(Mask_RenderToTexture); SceneUtil::ShadowManager::instance().disableShadowsForStateSet(*camera->getOrCreateStateSet()); } void apply(osg::Camera* camera) override { camera->setViewMatrix(mViewMatrix); camera->setCullMask(mNodeMask); } void setInterior(bool isInterior) { mInterior = isInterior; mNodeMask = calcNodeMask(); } 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))); } void setScene(osg::Node* scene) { if (mScene) mClipCullNode->removeChild(mScene); mScene = scene; mClipCullNode->addChild(scene); } 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; } osg::ref_ptr mClipCullNode; osg::ref_ptr mScene; osg::Node::NodeMask mNodeMask; osg::Matrix mViewMatrix{ osg::Matrix::identity() }; bool mInterior; }; /// DepthClampCallback enables GL_DEPTH_CLAMP for the current draw, if supported. class DepthClampCallback : public osg::Drawable::DrawCallback { 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; } glEnable(GL_DEPTH_CLAMP); drawable->drawImplementation(renderInfo); // restore default glDisable(GL_DEPTH_CLAMP); } }; 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) { mSimulation = std::make_unique(mSceneRoot, resourceSystem); 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"); mWaterNode = new osg::PositionAttitudeTransform; mWaterNode->setName("Water Root"); mWaterNode->addChild(mWaterGeom); mWaterNode->addCullCallback(new FudgeCallback); // 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); mSceneRoot->addChild(mWaterNode); setHeight(mTop); updateWaterMaterial(); if (ico) ico->add(mWaterNode); } void Water::setCullCallback(osg::Callback* callback) { if (mCullCallback) { mWaterNode->removeCullCallback(mCullCallback); if (mReflection) mReflection->removeCullCallback(mCullCallback); if (mRefraction) mRefraction->removeCullCallback(mCullCallback); } mCullCallback = callback; if (callback) { mWaterNode->addCullCallback(callback); if (mReflection) mReflection->addCullCallback(callback); if (mRefraction) mRefraction->addCullCallback(callback); } } 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->setStateSet(nullptr); mWaterGeom->setStateSet(nullptr); mWaterGeom->setUpdateCallback(nullptr); if (Settings::water().mShader) { const unsigned int rttSize = Settings::water().mRttSize; mReflection = new Reflection(rttSize, mInterior); mReflection->setWaterLevel(mTop); mReflection->setScene(mSceneRoot); if (mCullCallback) mReflection->addCullCallback(mCullCallback); mParent->addChild(mReflection); if (Settings::water().mRefraction) { mRefraction = new Refraction(rttSize); mRefraction->setWaterLevel(mTop); mRefraction->setScene(mSceneRoot); if (mCullCallback) mRefraction->addCullCallback(mCullCallback); mParent->addChild(mRefraction); } mRipples = new Ripples(mResourceSystem); mSimulation->setRipples(mRipples); mParent->addChild(mRipples); showWorld(mShowWorld); createShaderWaterStateSet(mWaterNode); } else createSimpleWaterStateSet(mWaterGeom, Fallback::Map::getFloat("Water_World_Alpha")); updateVisible(); } osg::Vec3d Water::getPosition() const { return mWaterNode->getPosition(); } void Water::createSimpleWaterStateSet(osg::Node* node, float alpha) { osg::ref_ptr stateset = SceneUtil::createSimpleWaterStateSet(alpha, MWRender::RenderBin_Water); node->setStateSet(stateset); node->setUpdateCallback(nullptr); mRainSettingsUpdater = nullptr; // 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) { 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); } 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::make_shared()); node->setUpdateCallback(controller); stateset->setTextureAttributeAndModes(0, textures[0], 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); } 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)) { } void setDefaults(osg::StateSet* stateset) override { stateset->addUniform(new osg::Uniform("normalMap", 0)); stateset->setTextureAttributeAndModes(0, mNormalMap, osg::StateAttribute::ON); stateset->setMode(GL_CULL_FACE, osg::StateAttribute::OFF); stateset->setAttributeAndModes(mProgram, osg::StateAttribute::ON); stateset->addUniform(new osg::Uniform("reflectionMap", 1)); if (mRefraction) { stateset->addUniform(new osg::Uniform("refractionMap", 2)); stateset->addUniform(new osg::Uniform("refractionDepthMap", 3)); stateset->setRenderBinDetails(MWRender::RenderBin_Default, "RenderBin"); } else { stateset->setMode(GL_BLEND, osg::StateAttribute::ON); stateset->setRenderBinDetails(MWRender::RenderBin_Water, "RenderBin"); osg::ref_ptr depth = 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 apply(osg::StateSet* stateset, osg::NodeVisitor* nv) override { osgUtil::CullVisitor* cv = static_cast(nv); stateset->setTextureAttributeAndModes(1, mReflection->getColorTexture(cv), osg::StateAttribute::ON); if (mRefraction) { stateset->setTextureAttributeAndModes(2, mRefraction->getColorTexture(cv), osg::StateAttribute::ON); stateset->setTextureAttributeAndModes(3, mRefraction->getDepthTexture(cv), osg::StateAttribute::ON); } if (mRipples) { stateset->setTextureAttributeAndModes(4, mRipples->getColorTexture(), osg::StateAttribute::ON); } stateset->getUniform("nodePosition")->set(osg::Vec3f(mWater->getPosition())); } private: Water* mWater; Reflection* mReflection; Refraction* mRefraction; Ripples* mRipples; osg::ref_ptr mProgram; osg::ref_ptr mNormalMap; }; 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"; Stereo::shaderStereoDefines(defineMap); Shader::ShaderManager& shaderMgr = mResourceSystem->getSceneManager()->getShaderManager(); osg::ref_ptr program = shaderMgr.getProgram("water", defineMap); 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); mRainSettingsUpdater = new RainSettingsUpdater(); node->setUpdateCallback(mRainSettingsUpdater); mShaderWaterStateSetUpdater = new ShaderWaterStateSetUpdater( this, mReflection, mRefraction, mRipples, std::move(program), std::move(normalMap)); node->addCullCallback(mShaderWaterStateSetUpdater); } void Water::processChangedSettings(const Settings::CategorySettingVector& settings) { updateWaterMaterial(); } Water::~Water() { 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) { 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) { 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); } void Water::setHeight(const float height) { mTop = height; mSimulation->setWaterHeight(height); osg::Vec3f pos = mWaterNode->getPosition(); pos.z() = height; mWaterNode->setPosition(pos); if (mReflection) mReflection->setWaterLevel(mTop); if (mRefraction) mRefraction->setWaterLevel(mTop); } void Water::setRainIntensity(float rainIntensity) { if (mRainSettingsUpdater) mRainSettingsUpdater->setRainIntensity(rainIntensity); } void Water::setRainRipplesEnabled(bool enableRipples) { if (mRainSettingsUpdater) mRainSettingsUpdater->setRipplesEnabled(enableRipples); } void Water::update(float dt, bool paused) { if (!paused) { mSimulation->update(dt); } if (mRipples) { mRipples->setPaused(paused); } } 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::toggle() { mToggled = !mToggled; updateVisible(); return mToggled; } bool Water::isUnderwater(const osg::Vec3f& pos) const { return pos.z() < mTop && mToggled && mEnabled; } 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::addEmitter(const MWWorld::Ptr& ptr, float scale, float force) { mSimulation->addEmitter(ptr, scale, force); } void Water::removeEmitter(const MWWorld::Ptr& ptr) { mSimulation->removeEmitter(ptr); } void Water::updateEmitterPtr(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) { mSimulation->updateEmitterPtr(old, ptr); } void Water::emitRipple(const osg::Vec3f& pos) { mSimulation->emitRipple(pos); } 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; } } openmw-openmw-0.49.0/apps/openmw/mwrender/water.hpp000066400000000000000000000062421503074453300223520ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_WATER_H #define OPENMW_MWRENDER_WATER_H #include #include #include #include #include #include #include namespace osg { class Group; class PositionAttitudeTransform; class Geometry; class Node; class Callback; } namespace osgUtil { class IncrementalCompileOperation; } namespace Resource { class ResourceSystem; } namespace MWWorld { class CellStore; class Ptr; } namespace Fallback { class Map; } namespace MWRender { class Refraction; class Reflection; class RippleSimulation; class RainSettingsUpdater; class Ripples; /// Water rendering class Water { osg::ref_ptr mRainSettingsUpdater; osg::ref_ptr mParent; osg::ref_ptr mSceneRoot; osg::ref_ptr mWaterNode; osg::ref_ptr mWaterGeom; Resource::ResourceSystem* mResourceSystem; osg::ref_ptr mIncrementalCompileOperation; std::unique_ptr mSimulation; osg::ref_ptr mRefraction; osg::ref_ptr mReflection; 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); void createShaderWaterStateSet(osg::Node* node); void updateWaterMaterial(); public: 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 setEnabled(bool enabled); bool toggle(); 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 emitRipple(const osg::Vec3f& pos); void removeCell(const MWWorld::CellStore* store); ///< remove all emitters in this cell void clearRipples(); void changeCell(const MWWorld::CellStore* store); void setHeight(const float height); void setRainIntensity(const float rainIntensity); void setRainRipplesEnabled(bool enableRipples); void update(float dt, bool paused); osg::Vec3d getPosition() const; void processChangedSettings(const Settings::CategorySettingVector& settings); void showWorld(bool show); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwrender/weaponanimation.cpp000066400000000000000000000176231503074453300244210ustar00rootroot00000000000000#include "weaponanimation.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/weapontype.hpp" #include "animation.hpp" #include "rotatecontroller.hpp" 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); } WeaponAnimation::WeaponAnimation() : mPitchFactor(0) { } WeaponAnimation::~WeaponAnimation() {} void WeaponAnimation::attachArrow(const 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->getType() != ESM::Weapon::sRecordId) return; 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::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); if (ammo == inv.end()) return; VFS::Path::Normalized model(ammo->getClass().getCorrectedModel(*ammo)); osg::ref_ptr arrow = getResourceSystem()->getSceneManager()->getInstance(model, parent); mAmmunition = std::make_unique(arrow); } } 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->getType() != ESM::Weapon::sRecordId) return; // The orientation of the launched projectile. Always the same as the actor orientation, even if the ArrowBone's // orientation dictates otherwise. 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().getESMStore()->get(); 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 Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot) { for (int i = 0; i < 2; ++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) { setControllerEnabled(false); return; } float pitch = characterPitchRadians * mPitchFactor; osg::Quat rotate(pitch / 2, osg::Vec3f(-1, 0, 0)); setControllerRotate(rotate); setControllerEnabled(true); } 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); } } openmw-openmw-0.49.0/apps/openmw/mwrender/weaponanimation.hpp000066400000000000000000000046261503074453300244250ustar00rootroot00000000000000#ifndef OPENMW_MWRENDER_WEAPONANIMATION_H #define OPENMW_MWRENDER_WEAPONANIMATION_H #include #include "../mwworld/ptr.hpp" #include "animation.hpp" namespace MWRender { class RotateController; class WeaponAnimationTime : public SceneUtil::ControllerSource { private: Animation* mAnimation; std::string mWeaponGroup; float mStartTime; bool mRelativeTime; public: WeaponAnimationTime(Animation* animation) : mAnimation(animation) , mStartTime(0) , mRelativeTime(false) { } void setGroup(const std::string& group, bool relativeTime); void updateStartTime(); float getValue(osg::NodeVisitor* nv) override; }; /// Handles attach & release of projectiles for ranged weapons class WeaponAnimation { public: WeaponAnimation(); virtual ~WeaponAnimation(); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. void attachArrow(const MWWorld::Ptr& actor); void detachArrow(MWWorld::Ptr actor); /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. 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 Animation::NodeMap& nodes, std::vector, osg::ref_ptr>>& map, osg::Node* objectRoot); void deleteControllers(); /// Configure controllers, should be called every animation frame. void configureControllers(float characterPitchRadians); protected: PartHolderPtr mAmmunition; osg::ref_ptr mSpineControllers[2]; void setControllerRotate(const osg::Quat& rotate); void setControllerEnabled(bool enabled); virtual osg::Group* getArrowBone() = 0; virtual osg::Node* getWeaponNode() = 0; virtual Resource::ResourceSystem* getResourceSystem() = 0; virtual void showWeapon(bool show) = 0; /// A relative factor (0-1) that decides if and how much the skeleton should be pitched /// to indicate the facing orientation of the character, for ranged weapon aiming. float mPitchFactor; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/000077500000000000000000000000001503074453300205405ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwscript/aiextensions.cpp000066400000000000000000000653471503074453300237740ustar00rootroot00000000000000#include "aiextensions.hpp" #include #include #include #include #include #include #include #include "../mwworld/class.hpp" #include "../mwworld/esmstore.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/creaturestats.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Ai { template class OpAiActivate : public Interpreter::Opcode1 { public: 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(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiActivate activatePackage(objectID, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(activatePackage, ptr); Log(Debug::Info) << "AiActivate"; } }; template class OpAiTravel : public Interpreter::Opcode1 { public: 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 y = runtime[0].mFloat; runtime.pop(); Interpreter::Type_Float z = runtime[0].mFloat; runtime.pop(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiTravel travelPackage(x, y, z, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(travelPackage, ptr); Log(Debug::Info) << "AiTravel: " << x << ", " << y << ", " << z; } }; template class OpAiEscort : public Interpreter::Opcode1 { public: 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(); Interpreter::Type_Float duration = 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(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiEscort escortPackage(actorID, static_cast(duration), x, y, z, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(escortPackage, ptr); Log(Debug::Info) << "AiEscort: " << x << ", " << y << ", " << z << ", " << duration; } }; template class OpAiEscortCell : public Interpreter::Opcode1 { public: 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_view cellID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = 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(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; if (cellID.empty()) return; if (!MWBase::Environment::get().getESMStore()->get().search(cellID)) return; 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; } }; template class OpGetAiPackageDone : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); bool done = false; if (ptr.getClass().isActor()) done = ptr.getClass().getCreatureStats(ptr).getAiSequence().isPackageDone(); runtime.push(done); } }; template class OpAiWander : public Interpreter::Opcode1 { public: 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 duration = static_cast(runtime[0].mFloat); runtime.pop(); Interpreter::Type_Integer time = static_cast(runtime[0].mFloat); runtime.pop(); // Chance for Idle is unused if (arg0) { --arg0; runtime.pop(); } 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; } 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(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiWander wanderPackage(range, duration, time, idleList, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(wanderPackage, ptr); } }; template class OpGetAiSetting : public Interpreter::Opcode0 { MWMechanics::AiSetting mIndex; public: OpGetAiSetting(MWMechanics::AiSetting index) : mIndex(index) { } 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 class OpModAiSetting : public Interpreter::Opcode0 { MWMechanics::AiSetting mIndex; public: OpModAiSetting(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()) 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 class OpSetAiSetting : public Interpreter::Opcode0 { MWMechanics::AiSetting mIndex; 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 class OpAiFollow : public Interpreter::Opcode1 { public: 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(); Interpreter::Type_Float duration = 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(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiFollow followPackage(actorID, duration, x, y, z, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(followPackage, ptr); Log(Debug::Info) << "AiFollow: " << actorID << ", " << x << ", " << y << ", " << z << ", " << duration; } }; template class OpAiFollowCell : public Interpreter::Opcode1 { public: 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_view cellID = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Float duration = 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(); // The value of the reset argument doesn't actually matter bool repeat = arg0; for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor() || ptr == MWMechanics::getPlayer()) return; MWMechanics::AiFollow followPackage(actorID, cellID, duration, x, y, z, repeat); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(followPackage, ptr); Log(Debug::Info) << "AiFollow: " << actorID << ", " << x << ", " << y << ", " << z << ", " << duration; } }; template class OpGetCurrentAIPackage : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = -1; if (ptr.getClass().isActor()) { const auto& stats = ptr.getClass().getCreatureStats(ptr); if (!stats.isDead() || !stats.isDeathAnimationFinished()) { value = static_cast(stats.getAiSequence().getLastRunTypeId()); } } runtime.push(value); } }; template class OpGetDetected : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr observer = R()(runtime, false); // required=false ESM::RefId actorID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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); runtime.push(value); } }; template class OpGetLineOfSight : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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); } }; template class OpGetTarget : public Interpreter::Opcode0 { 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); MWWorld::Ptr targetPtr; if (creatureStats.getAiSequence().getCombatTarget(targetPtr)) { if (!targetPtr.isEmpty() && targetPtr.getCellRef().getRefId() == testedTargetId) targetsAreEqual = true; } else if (testedTargetId == "Player") // Currently the player ID is hardcoded { MWBase::MechanicsManager* mechMgr = MWBase::Environment::get().getMechanicsManager(); bool greeting = mechMgr->getGreetingState(actor) == MWMechanics::Greet_InProgress; bool sayActive = MWBase::Environment::get().getSoundManager()->sayActive(actor); targetsAreEqual = (greeting && sayActive) || mechMgr->isTurningToPlayer(actor); } } runtime.push(targetsAreEqual); } }; template class OpStartCombat : public Interpreter::Opcode0 { 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() && !target.getBase()->isDeleted() && !target.getClass().getCreatureStats(target).isDead()) MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target, nullptr); } }; template class OpStopCombat : public Interpreter::Opcode0 { 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(); runtime.getContext().report(enabled ? "AI -> On" : "AI -> Off"); } }; template class OpFace : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = R()(runtime); Interpreter::Type_Float x = runtime[0].mFloat; runtime.pop(); 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) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/aiextensions.hpp000066400000000000000000000005261503074453300237650ustar00rootroot00000000000000#ifndef GAME_SCRIPT_AIEXTENSIONS_H #define GAME_SCRIPT_AIEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief AI-related script functionality namespace Ai { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/animationextensions.cpp000066400000000000000000000070751503074453300253540ustar00rootroot00000000000000#include "animationextensions.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "ref.hpp" namespace MWScript { namespace Animation { template class OpSkipAnim : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getMechanicsManager()->skipAnimation(ptr); } }; template class OpPlayAnim : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; std::string_view group = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); Interpreter::Type_Integer mode = 0; if (arg0 == 1) { mode = runtime[0].mInteger; runtime.pop(); 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); } }; template class OpLoopAnim : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; std::string_view group = runtime.getStringLiteral(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"); Interpreter::Type_Integer mode = 0; if (arg0 == 1) { mode = runtime[0].mInteger; runtime.pop(); if (mode < 0 || mode > 2) throw std::runtime_error("animation mode out of range"); } MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(ptr, group, mode, loops, true); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/animationextensions.hpp000066400000000000000000000006001503074453300253440ustar00rootroot00000000000000#ifndef GAME_SCRIPT_ANIMATIONEXTENSIONS_H #define GAME_SCRIPT_ANIMATIONEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Animation { void registerExtensions(Compiler::Extensions& extensions); void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/cellextensions.cpp000066400000000000000000000232161503074453300243070ustar00rootroot00000000000000#include "cellextensions.hpp" #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/actionteleport.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/scene.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWScript { namespace Cell { class OpCellChanged : public Interpreter::Opcode0 { 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 { 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; } bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); MWBase::Environment::get().getWorldScene()->testExteriorCells(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); } }; class OpTestInteriorCells : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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; } bool wasConsole = MWBase::Environment::get().getWindowManager()->isConsoleMode(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); MWBase::Environment::get().getWorldScene()->testInteriorCells(); if (wasConsole) MWBase::Environment::get().getWindowManager()->toggleConsole(); } }; class OpCOC : public Interpreter::Opcode0 { 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(); 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()) { 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(); 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 { if (!MWMechanics::getPlayer().isInCell()) { runtime.push(0); return; } bool interior = !MWMechanics::getPlayer().getCell()->getCell()->isExterior(); runtime.push(interior ? 1 : 0); } }; class OpGetPCCell : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { std::string_view 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_view current = MWBase::Environment::get().getWorld()->getCellName(cell); bool match = Misc::StringUtils::ciCompareLen(name, current, name.length()) == 0; runtime.push(match ? 1 : 0); } }; class OpGetWaterLevel : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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()); } }; class OpSetWaterLevel : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) { return; } MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); 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()); } }; class OpModWaterLevel : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { Interpreter::Type_Float level = runtime[0].mFloat; runtime.pop(); if (!MWMechanics::getPlayer().isInCell()) { return; } MWWorld::CellStore* cell = MWMechanics::getPlayer().getCell(); 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()); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/cellextensions.hpp000066400000000000000000000005361503074453300243140ustar00rootroot00000000000000#ifndef GAME_SCRIPT_CELLEXTENSIONS_H #define GAME_SCRIPT_CELLEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief cell-related script functionality namespace Cell { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/compilercontext.cpp000066400000000000000000000067241503074453300244740ustar00rootroot00000000000000#include "compilercontext.hpp" #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 "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/ptr.hpp" namespace MWScript { CompilerContext::CompilerContext(Type type) : mType(type) { } bool CompilerContext::canDeclareLocals() const { return mType == Type_Full; } char CompilerContext::getGlobalType(const std::string& name) const { return MWBase::Environment::get().getWorld()->getGlobalVariableType(name); } std::pair CompilerContext::getMemberType(const std::string& name, const ESM::RefId& id) const { ESM::RefId script; bool reference = false; if (const ESM::Script* scriptRecord = MWBase::Environment::get().getESMStore()->get().search(id)) { script = scriptRecord->mId; } else { MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), id); 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)); return std::make_pair(type, reference); } bool CompilerContext::isId(const ESM::RefId& name) const { 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); } } openmw-openmw-0.49.0/apps/openmw/mwscript/compilercontext.hpp000066400000000000000000000022741503074453300244750ustar00rootroot00000000000000#ifndef GAME_SCRIPT_COMPILERCONTEXT_H #define GAME_SCRIPT_COMPILERCONTEXT_H #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 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? }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/consoleextensions.cpp000066400000000000000000000003241503074453300250250ustar00rootroot00000000000000#include "consoleextensions.hpp" #include namespace MWScript { namespace Console { void installOpcodes(Interpreter::Interpreter& interpreter) {} } } openmw-openmw-0.49.0/apps/openmw/mwscript/consoleextensions.hpp000066400000000000000000000005611503074453300250350ustar00rootroot00000000000000#ifndef GAME_SCRIPT_CONSOLEEXTENSIONS_H #define GAME_SCRIPT_CONSOLEEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Script functionality limited to the console namespace Console { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/containerextensions.cpp000066400000000000000000000546011503074453300253540ustar00rootroot00000000000000#include "containerextensions.hpp" #include #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.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::ContainerStore& store, bool resolve = true) { if (itemPtr.getClass().getScript(itemPtr).empty()) { 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, true, resolve); } } void addRandomToStore(const MWWorld::Ptr& itemPtr, int count, MWWorld::ContainerStore& store, bool topLevel = true) { if (itemPtr.getType() == ESM::ItemLevList::sRecordId) { const ESM::ItemLevList* levItemList = itemPtr.get()->mBase; if (topLevel && count > 1 && levItemList->mFlags & ESM::ItemLevList::Each) { for (int i = 0; i < count; i++) addRandomToStore(itemPtr, 1, store, true); } else { 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().getESMStore(), itemId, 1); addRandomToStore(manualRef.getPtr(), count, store, false); } } else addToStore(itemPtr, count, store); } } namespace MWScript { namespace Container { template class OpAddItem : public Interpreter::Opcode0 { 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(); if (!MWBase::Environment::get().getESMStore()->find(item)) { runtime.getContext().report("Failed to add item '" + item.getRefIdString() + "': unknown ID"); return; } if (count < 0) count = static_cast(count); // no-op if (count == 0) return; if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") item = MWWorld::ContainerStore::sGoldId; // 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); // 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().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) { // use the new base record container.get()->mBase = baseRecord; if (container.getRefData().getCustomData()) { auto& store = container.getClass().getContainerStore(container); if (isLevelledList) { if (store.isResolved()) { addRandomToStore(itemPtr, count, store); } } else addToStore(itemPtr, count, store, store.isResolved()); } } 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 { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("#{sNotifyMessage61}"); msgBox = ::Misc::StringUtils::format(msgBox, count, itemName); } MWBase::Environment::get().getWindowManager()->messageBox(msgBox, MWGui::ShowInDialogueMode_Only); } } }; template class OpGetItemCount : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); ESM::RefId item = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (ptr.isEmpty() || (ptr.getType() != ESM::Container::sRecordId && !ptr.getClass().isActor())) { runtime.push(0); return; } if (item == "gold_005" || item == "gold_010" || item == "gold_025" || item == "gold_100") item = MWWorld::ContainerStore::sGoldId; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); runtime.push(store.count(item)); } }; template class OpRemoveItem : public Interpreter::Opcode0 { 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(); if (!MWBase::Environment::get().getESMStore()->find(item)) { runtime.getContext().report("Failed to remove item '" + item.getRefIdString() + "': unknown ID"); return; } if (count < 0) count = static_cast(count); // no-op if (count == 0) return; 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().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) { 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, false, false); } } return; } MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); std::string_view itemName; for (MWWorld::ConstContainerStoreIterator iter(store.cbegin()); iter != store.cend(); ++iter) { if (iter->getCellRef().getRefId() == item) { itemName = iter->getClass().getName(*iter); break; } } 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; 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: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId item = ESM::RefId::stringRefId(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(); // With soul gems we prefer filled ones. for (auto it = invStore.begin(); it != invStore.end(); ++it) { if (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 (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: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer location = runtime[0].mInteger; runtime.pop(); 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->getType() != ESM::Armor::sRecordId) { runtime.push(-1); return; } 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); 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) { MWWorld::ConstContainerStoreIterator it = invStore.getSlot(slot); if (it != invStore.end() && it->getCellRef().getRefId() == item) { runtime.push(1); return; } } runtime.push(0); } }; template class OpHasSoulGem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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 (it->getCellRef().getSoul() == name) count += it->getCellRef().getCount(); } runtime.push(count); } }; template class OpGetWeaponType : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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(-2); } else if (it->getType() == ESM::Probe::sRecordId) { runtime.push(-3); } else { runtime.push(-1); } return; } runtime.push(it->get()->mBase->mData.mType); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/containerextensions.hpp000066400000000000000000000006141503074453300253540ustar00rootroot00000000000000#ifndef GAME_SCRIPT_CONTAINEREXTENSIONS_H #define GAME_SCRIPT_CONTAINEREXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Container-related script functionality (chests, NPCs, creatures) namespace Container { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/controlextensions.cpp000066400000000000000000000252171503074453300250530ustar00rootroot00000000000000#include "controlextensions.hpp" #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" #include "../mwmechanics/creaturestats.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Control { class OpSetControl : public Interpreter::Opcode0 { std::string_view mControl; bool mEnable; 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); } }; class OpGetDisabled : public Interpreter::Opcode0 { std::string_view mControl; public: OpGetDisabled(std::string_view control) : mControl(control) { } 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(); runtime.getContext().report(enabled ? "Collision -> On" : "Collision -> Off"); } }; template class OpClearMovementFlag : public Interpreter::Opcode0 { MWMechanics::CreatureStats::Flag mFlag; public: OpClearMovementFlag(MWMechanics::CreatureStats::Flag flag) : mFlag(flag) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ptr.getClass().getCreatureStats(ptr).setMovementFlag(mFlag, false); } }; template class OpSetMovementFlag : public Interpreter::Opcode0 { MWMechanics::CreatureStats::Flag mFlag; public: OpSetMovementFlag(MWMechanics::CreatureStats::Flag flag) : mFlag(flag) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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); 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); 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); 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); 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(); 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)); } }; 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)); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/controlextensions.hpp000066400000000000000000000005621503074453300250540ustar00rootroot00000000000000#ifndef GAME_SCRIPT_CONTROLEXTENSIONS_H #define GAME_SCRIPT_CONTROLEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief player controls-related script functionality namespace Control { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/dialogueextensions.cpp000066400000000000000000000304161503074453300251610ustar00rootroot00000000000000#include "dialogueextensions.hpp" #include #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/journal.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "ref.hpp" namespace MWScript { namespace Dialogue { 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(); ESM::RefId quest = ESM::RefId::stringRefId(runtime.getStringLiteral(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); } } }; class OpSetJournalIndex : public Interpreter::Opcode0 { 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(); MWBase::Environment::get().getJournal()->setJournalIndex(quest, index); } }; class OpGetJournalIndex : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId quest = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int index = MWBase::Environment::get().getJournal()->getJournalIndex(quest); runtime.push(index); } }; class OpAddTopic : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId topic = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (!MWBase::Environment::get().getESMStore()->get().search(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 { 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) { choice = runtime[0].mInteger; runtime.pop(); arg0 = arg0 - 1; } dialogue->addChoice(question, choice); } } }; template class OpForceGreeting : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getRefData().isEnabled()) return; if (!ptr.getClass().isActor()) { const std::string error = "Warning: \"forcegreeting\" command works only for actors."; runtime.getContext().report(error); Log(Debug::Warning) << error; return; } bool greetWerewolves = false; const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) greetWerewolves = ptr.getRefData().getLocals().hasVar(script, "allowwerewolfforcegreeting"); 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); } }; class OpGoodbye : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getDialogueManager()->goodbye(); } }; 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(); ptr.getClass().getNpcStats(ptr).setReputation(ptr.getClass().getNpcStats(ptr).getReputation() + value); } }; 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(); ptr.getClass().getNpcStats(ptr).setReputation(value); } }; template class OpGetReputation : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getClass().getNpcStats(ptr).getReputation()); } }; template class OpSameFaction : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); runtime.push(player.getClass().getNpcStats(player).isInFaction(ptr.getClass().getPrimaryFaction(ptr))); } }; class OpModFactionReaction : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int modReaction = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getDialogueManager()->modFactionReaction(faction1, faction2, modReaction); } }; class OpGetFactionReaction : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); runtime.push(MWBase::Environment::get().getDialogueManager()->getFactionReaction(faction1, faction2)); } }; class OpSetFactionReaction : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId faction1 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ESM::RefId faction2 = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); int newValue = runtime[0].mInteger; runtime.pop(); MWBase::Environment::get().getDialogueManager()->setFactionReaction(faction1, faction2, newValue); } }; template class OpClearInfoActor : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getDialogueManager()->clearInfoActor(ptr); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/dialogueextensions.hpp000066400000000000000000000005661503074453300251710ustar00rootroot00000000000000#ifndef GAME_SCRIPT_DIALOGUEEXTENSIONS_H #define GAME_SCRIPT_DIALOGUEEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Dialogue/Journal-related script functionality namespace Dialogue { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/docs/000077500000000000000000000000001503074453300214705ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwscript/docs/vmformat.txt000066400000000000000000000370361503074453300240750ustar00rootroot00000000000000OpenMW Extensions: Segment 0: (not implemented yet) opcodes 0x20-0x3f unused Segment 1: (not implemented yet) opcodes 0x20-0x3f unused Segment 2: (not implemented yet) opcodes 0x200-0x3ff unused Segment 3: op 0x20000: AiTravel op 0x20001: AiTravel, explicit reference op 0x20002: AiEscort op 0x20003: AiEscort, explicit reference op 0x20004: Lock op 0x20005: Lock, explicit reference op 0x20006: PlayAnim op 0x20007: PlayAnim, explicit reference op 0x20008: LoopAnim op 0x20009: LoopAnim, explicit reference op 0x2000a: Choice op 0x2000b: PCRaiseRank op 0x2000c: PCLowerRank op 0x2000d: PCJoinFaction op 0x2000e: PCGetRank implicit op 0x2000f: PCGetRank explicit op 0x20010: AiWander op 0x20011: AiWander, explicit reference op 0x20012: GetPCFacRep op 0x20013: GetPCFacRep, explicit reference op 0x20014: SetPCFacRep op 0x20015: SetPCFacRep, explicit reference op 0x20016: ModPCFacRep op 0x20017: ModPCFacRep, explicit reference op 0x20018: PcExpelled op 0x20019: PcExpelled, explicit op 0x2001a: PcExpell op 0x2001b: PcExpell, explicit op 0x2001c: PcClearExpelled op 0x2001d: PcClearExpelled, explicit op 0x2001e: AIActivate op 0x2001f: AIActivate, explicit reference op 0x20020: AiEscortCell op 0x20021: AiEscortCell, explicit reference op 0x20022: AiFollow op 0x20023: AiFollow, explicit reference op 0x20024: AiFollowCell op 0x20025: AiFollowCell, explicit reference op 0x20026: ModRegion op 0x20027: RemoveSoulGem op 0x20028: RemoveSoulGem, explicit reference op 0x20029: PCRaiseRank, explicit reference op 0x2002a: PCLowerRank, explicit reference op 0x2002b: PCJoinFaction, explicit reference op 0x2002c: MenuTest op 0x2002d: BetaComment op 0x2002e: BetaComment, explicit reference op 0x2002f: ShowSceneGraph op 0x20030: ShowSceneGraph, explicit opcodes 0x20031-0x3ffff unused Segment 4: (not implemented yet) opcodes 0x200-0x3ff unused Segment 5: op 0x2000000: CellChanged op 0x2000001: Say op 0x2000002: SayDone op 0x2000003: StreamMusic op 0x2000004: PlaySound op 0x2000005: PlaySoundVP op 0x2000006: PlaySound3D op 0x2000007: PlaySound3DVP op 0x2000008: PlayLoopSound3D op 0x2000009: PlayLoopSound3DVP op 0x200000a: StopSound op 0x200000b: GetSoundPlaying op 0x200000c: XBox (always 0) op 0x200000d: OnActivate op 0x200000e: EnableBirthMenu op 0x200000f: EnableClassMenu op 0x2000010: EnableNameMenu op 0x2000011: EnableRaceMenu op 0x2000012: EnableStatsReviewMenu op 0x2000013: EnableInventoryMenu op 0x2000014: EnableMagicMenu op 0x2000015: EnableMapMenu op 0x2000016: EnableStatsMenu op 0x2000017: EnableRest op 0x2000018: ShowRestMenu op 0x2000019: Say, explicit reference op 0x200001a: SayDone, explicit reference op 0x200001b: PlaySound3D, explicit reference op 0x200001c: PlaySound3DVP, explicit reference op 0x200001d: PlayLoopSound3D, explicit reference op 0x200001e: PlayLoopSound3DVP, explicit reference op 0x200001f: StopSound, explicit reference op 0x2000020: GetSoundPlaying, explicit reference op 0x2000021: ToggleSky op 0x2000022: TurnMoonWhite op 0x2000023: TurnMoonRed op 0x2000024: GetMasserPhase op 0x2000025: GetSecundaPhase op 0x2000026: COC op 0x2000027-0x200002e: GetAttribute op 0x200002f-0x2000036: GetAttribute, explicit reference op 0x2000037-0x200003e: SetAttribute op 0x200003f-0x2000046: SetAttribute, explicit reference op 0x2000047-0x200004e: ModAttribute op 0x200004f-0x2000056: ModAttribute, explicit reference op 0x2000057-0x2000059: GetDynamic (health, magicka, fatigue) op 0x200005a-0x200005c: GetDynamic (health, magicka, fatigue), explicit reference op 0x200005d-0x200005f: SetDynamic (health, magicka, fatigue) op 0x2000060-0x2000062: SetDynamic (health, magicka, fatigue), explicit reference op 0x2000063-0x2000065: ModDynamic (health, magicka, fatigue) op 0x2000066-0x2000068: ModDynamic (health, magicka, fatigue), explicit reference op 0x2000069-0x200006b: ModDynamic (health, magicka, fatigue) op 0x200006c-0x200006e: ModDynamic (health, magicka, fatigue), explicit reference op 0x200006f-0x2000071: GetDynamic (health, magicka, fatigue) op 0x2000072-0x2000074: GetDynamic (health, magicka, fatigue), explicit reference op 0x2000075: Activate op 0x2000076: AddItem op 0x2000077: AddItem, explicit reference op 0x2000078: GetItemCount op 0x2000079: GetItemCount, explicit reference op 0x200007a: RemoveItem op 0x200007b: RemoveItem, explicit reference op 0x200007c: GetAiPackageDone op 0x200007d: GetAiPackageDone, explicit reference op 0x200007e-0x2000084: Enable Controls op 0x2000085-0x200008b: Disable Controls op 0x200008c: Unlock op 0x200008d: Unlock, explicit reference op 0x200008e-0x20000a8: GetSkill op 0x20000a9-0x20000c3: GetSkill, explicit reference op 0x20000c4-0x20000de: SetSkill op 0x20000df-0x20000f9: SetSkill, explicit reference op 0x20000fa-0x2000114: ModSkill op 0x2000115-0x200012f: ModSKill, explicit reference op 0x2000130: ToggleCollision op 0x2000131: GetInterior op 0x2000132: ToggleCollsionDebug op 0x2000133: Journal op 0x2000134: SetJournalIndex op 0x2000135: GetJournalIndex op 0x2000136: GetPCCell op 0x2000137: GetButtonPressed op 0x2000138: SkipAnim op 0x2000139: SkipAnim, expplicit reference op 0x200013a: AddTopic op 0x200013b: twf op 0x200013c: FadeIn op 0x200013d: FadeOut op 0x200013e: FadeTo op 0x200013f: GetCurrentWeather op 0x2000140: ChangeWeather op 0x2000141: GetWaterLevel op 0x2000142: SetWaterLevel op 0x2000143: ModWaterLevel op 0x2000144: ToggleWater, twa op 0x2000145: ToggleFogOfWar (tfow) op 0x2000146: TogglePathgrid op 0x2000147: AddSpell op 0x2000148: AddSpell, explicit reference op 0x2000149: RemoveSpell op 0x200014a: RemoveSpell, explicit reference op 0x200014b: GetSpell op 0x200014c: GetSpell, explicit reference op 0x200014d: ModDisposition op 0x200014e: ModDisposition, explicit reference op 0x200014f: ForceGreeting op 0x2000150: ForceGreeting, explicit reference op 0x2000151: ToggleFullHelp op 0x2000152: Goodbye op 0x2000153: DontSaveObject (left unimplemented) op 0x2000154: ClearForceRun op 0x2000155: ClearForceRun, explicit reference op 0x2000156: ForceRun op 0x2000157: ForceRun, explicit reference op 0x2000158: ClearForceSneak op 0x2000159: ClearForceSneak, explicit reference op 0x200015a: ForceSneak op 0x200015b: ForceSneak, explicit reference op 0x200015c: SetHello op 0x200015d: SetHello, explicit reference op 0x200015e: SetFight op 0x200015f: SetFight, explicit reference op 0x2000160: SetFlee op 0x2000161: SetFlee, explicit reference op 0x2000162: SetAlarm op 0x2000163: SetAlarm, explicit reference op 0x2000164: SetScale op 0x2000165: SetScale, explicit reference op 0x2000166: SetAngle op 0x2000167: SetAngle, explicit reference op 0x2000168: GetScale op 0x2000169: GetScale, explicit reference op 0x200016a: GetAngle op 0x200016b: GetAngle, explicit reference op 0x200016c: user1 (console only, requires --script-console switch) op 0x200016d: user2 (console only, requires --script-console switch) op 0x200016e: user3, explicit reference (console only, requires --script-console switch) op 0x200016f: user3 (implicit reference, console only, requires --script-console switch) op 0x2000170: user4, explicit reference (console only, requires --script-console switch) op 0x2000171: user4 (implicit reference, console only, requires --script-console switch) op 0x2000172: GetStartingAngle op 0x2000173: GetStartingAngle, explicit reference op 0x2000174: ToggleVanityMode op 0x2000175-0x200018B: Get controls disabled op 0x200018C: GetLevel op 0x200018D: GetLevel, explicit reference op 0x200018E: SetLevel op 0x200018F: SetLevel, explicit reference op 0x2000190: GetPos op 0x2000191: GetPosExplicit op 0x2000192: SetPos op 0x2000193: SetPosExplicit op 0x2000194: GetStartingPos op 0x2000195: GetStartingPosExplicit op 0x2000196: Position op 0x2000197: Position Explicit op 0x2000198: PositionCell op 0x2000199: PositionCell Explicit op 0x200019a: PlaceItemCell op 0x200019b: PlaceItem op 0x200019c: PlaceAtPc op 0x200019d: PlaceAtMe op 0x200019e: PlaceAtMe Explicit op 0x200019f: GetPcSleep op 0x20001a0: ShowMap op 0x20001a1: FillMap op 0x20001a2: WakeUpPc op 0x20001a3: GetDeadCount op 0x20001a4: SetDisposition op 0x20001a5: SetDisposition, Explicit op 0x20001a6: GetDisposition op 0x20001a7: GetDisposition, Explicit op 0x20001a8: CommonDisease op 0x20001a9: CommonDisease, explicit reference op 0x20001aa: BlightDisease op 0x20001ab: BlightDisease, explicit reference op 0x20001ac: ToggleCollisionBoxes op 0x20001ad: SetReputation op 0x20001ae: ModReputation op 0x20001af: SetReputation, explicit op 0x20001b0: ModReputation, explicit op 0x20001b1: GetReputation op 0x20001b2: GetReputation, explicit op 0x20001b3: Equip op 0x20001b4: Equip, explicit op 0x20001b5: SameFaction op 0x20001b6: SameFaction, explicit op 0x20001b7: ModHello op 0x20001b8: ModHello, explicit reference op 0x20001b9: ModFight op 0x20001ba: ModFight, explicit reference op 0x20001bb: ModFlee op 0x20001bc: ModFlee, explicit reference op 0x20001bd: ModAlarm op 0x20001be: ModAlarm, explicit reference op 0x20001bf: GetHello op 0x20001c0: GetHello, explicit reference op 0x20001c1: GetFight op 0x20001c2: GetFight, explicit reference op 0x20001c3: GetFlee op 0x20001c4: GetFlee, explicit reference op 0x20001c5: GetAlarm op 0x20001c6: GetAlarm, explicit reference op 0x20001c7: GetLocked op 0x20001c8: GetLocked, explicit reference op 0x20001c9: GetPcRunning op 0x20001ca: GetPcSneaking op 0x20001cb: GetForceRun op 0x20001cc: GetForceSneak op 0x20001cd: GetForceRun, explicit op 0x20001ce: GetForceSneak, explicit op 0x20001cf: GetEffect op 0x20001d0: GetEffect, explicit op 0x20001d1: GetArmorType op 0x20001d2: GetArmorType, explicit op 0x20001d3: GetAttacked op 0x20001d4: GetAttacked, explicit op 0x20001d5: HasItemEquipped op 0x20001d6: HasItemEquipped, explicit op 0x20001d7: GetWeaponDrawn op 0x20001d8: GetWeaponDrawn, explicit op 0x20001d9: GetRace op 0x20001da: GetRace, explicit op 0x20001db: GetSpellEffects op 0x20001dc: GetSpellEffects, explicit op 0x20001dd: GetCurrentTime op 0x20001de: HasSoulGem op 0x20001df: HasSoulGem, explicit op 0x20001e0: GetWeaponType op 0x20001e1: GetWeaponType, explicit op 0x20001e2: GetWerewolfKills op 0x20001e3: ModScale op 0x20001e4: ModScale, explicit op 0x20001e5: SetDelete op 0x20001e6: SetDelete, explicit op 0x20001e7: GetSquareRoot op 0x20001e8: RaiseRank op 0x20001e9: RaiseRank, explicit op 0x20001ea: LowerRank op 0x20001eb: LowerRank, explicit op 0x20001ec: GetPCCrimeLevel op 0x20001ed: SetPCCrimeLevel op 0x20001ee: ModPCCrimeLevel op 0x20001ef: GetCurrentAIPackage op 0x20001f0: GetCurrentAIPackage, explicit reference op 0x20001f1: GetDetected op 0x20001f2: GetDetected, explicit reference op 0x20001f3: AddSoulGem op 0x20001f4: AddSoulGem, explicit reference op 0x20001f5: unused op 0x20001f6: unused op 0x20001f7: PlayBink op 0x20001f8: Drop op 0x20001f9: Drop, explicit reference op 0x20001fa: DropSoulGem op 0x20001fb: DropSoulGem, explicit reference op 0x20001fc: OnDeath op 0x20001fd: IsWerewolf op 0x20001fe: IsWerewolf, explicit reference op 0x20001ff: Rotate op 0x2000200: Rotate, explicit reference op 0x2000201: RotateWorld op 0x2000202: RotateWorld, explicit reference op 0x2000203: SetAtStart op 0x2000204: SetAtStart, explicit op 0x2000205: OnDeath, explicit op 0x2000206: Move op 0x2000207: Move, explicit op 0x2000208: MoveWorld op 0x2000209: MoveWorld, explicit op 0x200020a: Fall op 0x200020b: Fall, explicit op 0x200020c: GetStandingPC op 0x200020d: GetStandingPC, explicit op 0x200020e: GetStandingActor op 0x200020f: GetStandingActor, explicit op 0x2000210: GetStartingAngle op 0x2000211: GetStartingAngle, explicit op 0x2000212: GetWindSpeed op 0x2000213: HitOnMe op 0x2000214: HitOnMe, explicit op 0x2000215: DisableTeleporting op 0x2000216: EnableTeleporting op 0x2000217: BecomeWerewolf op 0x2000218: BecomeWerewolfExplicit op 0x2000219: UndoWerewolf op 0x200021a: UndoWerewolfExplicit op 0x200021b: SetWerewolfAcrobatics op 0x200021c: SetWerewolfAcrobaticsExplicit op 0x200021d: ShowVars op 0x200021e: ShowVarsExplicit op 0x200021f: ToggleGodMode op 0x2000220: DisableLevitation op 0x2000221: EnableLevitation op 0x2000222: GetLineOfSight op 0x2000223: GetLineOfSightExplicit op 0x2000224: ToggleAI op 0x2000225: unused op 0x2000226: COE op 0x2000227: Cast op 0x2000228: Cast, explicit op 0x2000229: ExplodeSpell op 0x200022a: ExplodeSpell, explicit op 0x200022b: RemoveSpellEffects op 0x200022c: RemoveSpellEffects, explicit op 0x200022d: RemoveEffects op 0x200022e: RemoveEffects, explicit op 0x200022f: Resurrect op 0x2000230: Resurrect, explicit op 0x2000231: GetSpellReadied op 0x2000232: GetSpellReadied, explicit op 0x2000233: GetPcJumping op 0x2000234: ShowRestMenu, explicit op 0x2000235: GoToJail op 0x2000236: PayFine op 0x2000237: PayFineThief op 0x2000238: GetTarget op 0x2000239: GetTargetExplicit op 0x200023a: StartCombat op 0x200023b: StartCombatExplicit op 0x200023c: StopCombat op 0x200023d: StopCombatExplicit op 0x200023e: GetPcInJail op 0x200023f: GetPcTraveling op 0x2000240: onKnockout op 0x2000241: onKnockoutExplicit op 0x2000242: ModFactionReaction op 0x2000243: GetFactionReaction op 0x2000244: Activate, explicit op 0x2000245: ClearInfoActor op 0x2000246: ClearInfoActor, explicit op 0x2000247: (unused) op 0x2000248: (unused) op 0x2000249: OnMurder op 0x200024a: OnMurder, explicit op 0x200024b: ToggleMenus op 0x200024c: Face op 0x200024d: Face, explicit op 0x200024e: GetStat (dummy function) op 0x200024f: GetStat (dummy function), explicit op 0x2000250: GetCollidingPC op 0x2000251: GetCollidingPC, explicit op 0x2000252: GetCollidingActor op 0x2000253: GetCollidingActor, explicit op 0x2000254: HurtStandingActor op 0x2000255: HurtStandingActor, explicit op 0x2000256: HurtCollidingActor op 0x2000257: HurtCollidingActor, explicit op 0x2000258: ClearForceJump op 0x2000259: ClearForceJump, explicit reference op 0x200025a: ForceJump op 0x200025b: ForceJump, explicit reference op 0x200025c: ClearForceMoveJump op 0x200025d: ClearForceMoveJump, explicit reference op 0x200025e: ForceMoveJump op 0x200025f: ForceMoveJump, explicit reference op 0x2000260: GetForceJump op 0x2000261: GetForceJump, explicit reference op 0x2000262: GetForceMoveJump op 0x2000263: GetForceMoveJump, explicit reference op 0x2000264-0x200027b: GetMagicEffect op 0x200027c-0x2000293: GetMagicEffect, explicit op 0x2000294-0x20002ab: SetMagicEffect op 0x20002ac-0x20002c3: SetMagicEffect, explicit op 0x20002c4-0x20002db: ModMagicEffect op 0x20002dc-0x20002f3: ModMagicEffect, explicit op 0x20002f4: ResetActors op 0x20002f5: ToggleWorld op 0x20002f6: PCForce1stPerson op 0x20002f7: PCForce3rdPerson op 0x20002f8: PCGet3rdPerson op 0x20002f9: HitAttemptOnMe op 0x20002fa: HitAttemptOnMe, explicit op 0x20002fb: AddToLevCreature op 0x20002fc: RemoveFromLevCreature op 0x20002fd: AddToLevItem op 0x20002fe: RemoveFromLevItem op 0x20002ff: SetFactionReaction op 0x2000300: EnableLevelupMenu op 0x2000301: ToggleScripts op 0x2000302: Fixme op 0x2000303: Fixme, explicit op 0x2000304: Show op 0x2000305: Show, explicit op 0x2000306: OnActivate, explicit op 0x2000307: ToggleBorders, tb op 0x2000308: ToggleNavMesh op 0x2000309: ToggleActorsPaths op 0x200030a: SetNavMeshNumber op 0x200030b: Journal, explicit op 0x200030c: RepairedOnMe op 0x200030d: RepairedOnMe, explicit op 0x200030e: TestCells op 0x200030f: TestInteriorCells op 0x2000310: ToggleRecastMesh op 0x2000311: MenuMode op 0x2000312: Random op 0x2000313: ScriptRunning op 0x2000314: StartScript op 0x2000315: StopScript op 0x2000316: GetSecondsPassed op 0x2000317: Enable op 0x2000318: Disable op 0x2000319: GetDisabled op 0x200031a: Enable, explicit op 0x200031b: Disable, explicit 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 0x2000326-0x3ffffff unused openmw-openmw-0.49.0/apps/openmw/mwscript/extensions.cpp000066400000000000000000000026261503074453300234510ustar00rootroot00000000000000#include "extensions.hpp" #include #include #include "aiextensions.hpp" #include "animationextensions.hpp" #include "cellextensions.hpp" #include "consoleextensions.hpp" #include "containerextensions.hpp" #include "controlextensions.hpp" #include "dialogueextensions.hpp" #include "guiextensions.hpp" #include "miscextensions.hpp" #include "skyextensions.hpp" #include "soundextensions.hpp" #include "statsextensions.hpp" #include "transformationextensions.hpp" #include "userextensions.hpp" namespace MWScript { 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); if (consoleOnly) { Console::installOpcodes(interpreter); User::installOpcodes(interpreter); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/extensions.hpp000066400000000000000000000005251503074453300234520ustar00rootroot00000000000000#ifndef GAME_SCRIPT_EXTENSIONS_H #define GAME_SCRIPT_EXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { void installOpcodes(Interpreter::Interpreter& interpreter, bool consoleOnly = false); ///< \param consoleOnly include console only opcodes } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/globalscripts.cpp000066400000000000000000000225031503074453300241160ustar00rootroot00000000000000#include "globalscripts.hpp" #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/worldmodel.hpp" #include "interpretercontext.hpp" namespace { struct ScriptCreatingVisitor { ESM::GlobalScript operator()(const MWWorld::Ptr& ptr) const { ESM::GlobalScript script; script.mRunning = false; if (!ptr.isEmpty()) { if (ptr.getCellRef().hasContentFile()) { script.mTargetId = ptr.getCellRef().getRefId(); script.mTargetRef = ptr.getCellRef().getRefNum(); } else if (MWBase::Environment::get().getWorld()->getPlayerPtr() == ptr) script.mTargetId = ptr.getCellRef().getRefId(); } return script; } ESM::GlobalScript operator()(const std::pair& pair) const { ESM::GlobalScript script; script.mTargetId = pair.second; script.mTargetRef = pair.first; script.mRunning = false; return script; } }; struct PtrGettingVisitor { const MWWorld::Ptr* operator()(const MWWorld::Ptr& ptr) const { return &ptr; } const MWWorld::Ptr* operator()(const std::pair& pair) const { return nullptr; } }; struct PtrResolvingVisitor { MWWorld::Ptr operator()(const MWWorld::Ptr& ptr) const { return ptr; } MWWorld::Ptr operator()(const std::pair& pair) const { if (pair.second.empty()) return MWWorld::Ptr(); else if (pair.first.hasContentFile()) return MWBase::Environment::get().getWorldModel()->getPtr(pair.first); return MWBase::Environment::get().getWorld()->searchPtr(pair.second, false); } }; class MatchPtrVisitor { const MWWorld::Ptr& mPtr; public: MatchPtrVisitor(const MWWorld::Ptr& ptr) : mPtr(ptr) { } 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 { 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) { } const MWWorld::Ptr* GlobalScriptDesc::getPtrIfPresent() const { return std::visit(PtrGettingVisitor(), mTarget); } MWWorld::Ptr GlobalScriptDesc::getPtr() { 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) { } void GlobalScripts::addScript(const ESM::RefId& name, const MWWorld::Ptr& target) { const auto iter = mScripts.find(name); if (iter == mScripts.end()) { 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)); } else { Log(Debug::Error) << "Failed to add global script " << name << ": script record not found"; } } else if (!iter->second->mRunning) { iter->second->mRunning = true; MWWorld::Ptr ptr = target; iter->second->mTarget = ptr; } } void GlobalScripts::removeScript(const ESM::RefId& name) { const auto iter = mScripts.find(name); if (iter != mScripts.end()) iter->second->mRunning = false; } bool GlobalScripts::isRunning(const ESM::RefId& name) const { const auto iter = mScripts.find(name); if (iter == mScripts.end()) return false; return iter->second->mRunning; } void GlobalScripts::run() { for (const auto& script : mScripts) { if (script.second->mRunning) { MWScript::InterpreterContext context(script.second); if (!MWBase::Environment::get().getScriptManager()->run(script.first, context)) script.second->mRunning = false; } } } void GlobalScripts::clear() { mScripts.clear(); } void GlobalScripts::addStartup() { // make list of global scripts to be added std::vector scripts; scripts.emplace_back(ESM::RefId::stringRefId("main")); for (MWWorld::Store::iterator iter = mStore.get().begin(); iter != mStore.get().end(); ++iter) { scripts.push_back(iter->mId); } // add scripts for (auto iter(scripts.begin()); iter != scripts.end(); ++iter) { try { addScript(*iter); } catch (const std::exception& exception) { Log(Debug::Error) << "Failed to add start script " << *iter << " because an exception has " << "been thrown: " << exception.what(); } } } int GlobalScripts::countSavedGameRecords() const { return mScripts.size(); } void GlobalScripts::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { for (const auto& [id, desc] : mScripts) { ESM::GlobalScript script = std::visit(ScriptCreatingVisitor{}, desc->mTarget); script.mId = id; desc->mLocals.write(script.mLocals, id); script.mRunning = desc->mRunning; writer.startRecord(ESM::REC_GSCR); script.save(writer); writer.endRecord(ESM::REC_GSCR); } } bool GlobalScripts::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GSCR) { ESM::GlobalScript script; script.load(reader); auto iter = mScripts.find(script.mId); if (iter == mScripts.end()) { if (const ESM::Script* scriptRecord = mStore.get().search(script.mId)) { try { auto desc = std::make_shared(); if (!script.mTargetId.empty()) { desc->mTarget = std::make_pair(script.mTargetRef, script.mTargetId); } desc->mLocals.configure(*scriptRecord); 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(); return true; } } else // script does not exist anymore return true; } iter->second->mRunning = script.mRunning; iter->second->mLocals.read(script.mLocals, script.mId); return true; } return false; } Locals& GlobalScripts::getLocals(const ESM::RefId& name) { auto iter = mScripts.find(name); if (iter == mScripts.end()) { const ESM::Script* script = mStore.get().find(name); auto desc = std::make_shared(); desc->mLocals.configure(*script); iter = mScripts.emplace(name, desc).first; } return iter->second->mLocals; } const GlobalScriptDesc* GlobalScripts::getScriptIfPresent(const ESM::RefId& name) const { auto iter = mScripts.find(name); if (iter == mScripts.end()) return nullptr; return iter->second.get(); } void GlobalScripts::updatePtrs(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) { MatchPtrVisitor visitor(base); for (const auto& script : mScripts) { if (std::visit(visitor, script.second->mTarget)) script.second->mTarget = updated; } } } openmw-openmw-0.49.0/apps/openmw/mwscript/globalscripts.hpp000066400000000000000000000046271503074453300241320ustar00rootroot00000000000000#ifndef GAME_SCRIPT_GLOBALSCRIPTS_H #define GAME_SCRIPT_GLOBALSCRIPTS_H #include #include #include #include #include #include #include #include #include #include #include "locals.hpp" #include "../mwworld/ptr.hpp" namespace ESM { class ESMWriter; class ESMReader; struct FormId; using RefNum = FormId; } namespace Loading { class Listener; } namespace MWWorld { class ESMStore; } namespace MWScript { struct GlobalScriptDesc { bool mRunning; Locals mLocals; 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::unordered_map> mScripts; public: GlobalScripts(const MWWorld::ESMStore& store); void addScript(const ESM::RefId& name, const MWWorld::Ptr& target = MWWorld::Ptr()); void removeScript(const ESM::RefId& name); bool isRunning(const ESM::RefId& name) const; void run(); ///< run all active global scripts void clear(); void addStartup(); ///< Add startup script int countSavedGameRecords() const; 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? 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 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. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/guiextensions.cpp000066400000000000000000000221111503074453300241450ustar00rootroot00000000000000#include "guiextensions.hpp" #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "ref.hpp" namespace MWScript { namespace Gui { class OpEnableWindow : public Interpreter::Opcode0 { MWGui::GuiWindow mWindow; public: OpEnableWindow(MWGui::GuiWindow window) : mWindow(window) { } 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(); } }; template class OpShowRestMenu : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr bed = R()(runtime, false); 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; public: OpShowDialogue(MWGui::GuiMode dialogue) : mDialogue(dialogue) { } 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()); } }; 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"); } }; 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"); } }; class OpShowMap : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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 const MWWorld::Store& cells = MWBase::Environment::get().getESMStore()->get(); MWBase::WindowManager* winMgr = MWBase::Environment::get().getWindowManager(); for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) { const auto& cellName = it->mName; if (Misc::StringUtils::ciStartsWith(cellName, cell)) winMgr->addVisitedLocation(cellName, it->getGridX(), it->getGridY()); } } }; class OpFillMap : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const MWWorld::Store& cells = MWBase::Environment::get().getESMStore()->get(); for (auto it = cells.extBegin(); it != cells.extEnd(); ++it) { const std::string& name = it->mName; if (!name.empty()) MWBase::Environment::get().getWindowManager()->addVisitedLocation( name, it->getGridX(), it->getGridY()); } } }; class OpMenuTest : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { 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) { if (MWBase::Environment::get().getWindowManager()->containsMode(modes[i])) MWBase::Environment::get().getWindowManager()->removeGuiMode(modes[i]); } } else { MWGui::GuiWindow gw = MWGui::GW_None; if (arg == 3) gw = MWGui::GW_Stats; if (arg == 4) gw = MWGui::GW_Inventory; if (arg == 5) gw = MWGui::GW_Magic; if (arg == 6) gw = MWGui::GW_Map; MWBase::Environment::get().getWindowManager()->pinWindow(gw); } } }; class OpToggleMenus : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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! MWBase::Environment::get().getWindowManager()->popGuiMode(); } } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/guiextensions.hpp000066400000000000000000000005321503074453300241550ustar00rootroot00000000000000#ifndef GAME_SCRIPT_GUIEXTENSIONS_H #define GAME_SCRIPT_GUIEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief GUI-related script functionality namespace Gui { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/interpretercontext.cpp000066400000000000000000000403141503074453300252160ustar00rootroot00000000000000#include "interpretercontext.hpp" #include #include #include #include #include "../mwworld/esmstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/action.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" #include "../mwmechanics/npcstats.hpp" #include "globalscripts.hpp" #include "locals.hpp" namespace MWScript { 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); } else { if (mReference.isEmpty() && mGlobalScriptDesc) mReference = mGlobalScriptDesc->getPtr(); if (mReference.isEmpty() && doThrow) throw MissingImplicitRefError(); return mReference; } } const Locals& InterpreterContext::getMemberLocals(bool global, ESM::RefId& id) const { if (global) { return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(id); } else { const MWWorld::Ptr ptr = getReferenceImp(id, false); id = ptr.getClass().getScript(ptr); ptr.getRefData().setLocals(*MWBase::Environment::get().getESMStore()->get().find(id)); return ptr.getRefData().getLocals(); } } Locals& InterpreterContext::getMemberLocals(bool global, ESM::RefId& id) { if (global) { return MWBase::Environment::get().getScriptManager()->getGlobalScripts().getLocals(id); } else { const MWWorld::Ptr ptr = getReferenceImp(id, false); id = ptr.getClass().getScript(ptr); ptr.getRefData().setLocals(*MWBase::Environment::get().getESMStore()->get().find(id)); return ptr.getRefData().getLocals(); } } MissingImplicitRefError::MissingImplicitRefError() : std::runtime_error("no implicit reference") { } 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); if (index != -1) return index; std::ostringstream stream; stream << "Failed to access "; switch (type) { 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()); } InterpreterContext::InterpreterContext(MWScript::Locals* locals, const MWWorld::Ptr& reference) : mLocals(locals) , mReference(reference) { } 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. // Script targets are lazily resolved to MWWorld::Ptrs (which can, upon resolution, be empty) // because scripts started through dialogue often don't use their implicit target. if (ptr) mReference = *ptr; else mGlobalScriptDesc = globalScriptDesc; } 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"); return mLocals->mShorts.at(index); } int InterpreterContext::getLocalLong(int index) const { if (!mLocals) throw std::runtime_error("local variables not available in this context"); return mLocals->mLongs.at(index); } float InterpreterContext::getLocalFloat(int index) const { if (!mLocals) throw std::runtime_error("local variables not available in this context"); return mLocals->mFloats.at(index); } void InterpreterContext::setLocalShort(int index, int value) { if (!mLocals) throw std::runtime_error("local variables not available in this context"); mLocals->mShorts.at(index) = value; } void InterpreterContext::setLocalLong(int index, int value) { if (!mLocals) throw std::runtime_error("local variables not available in this context"); mLocals->mLongs.at(index) = value; } void InterpreterContext::setLocalFloat(int index, float value) { if (!mLocals) throw std::runtime_error("local variables not available in this context"); mLocals->mFloats.at(index) = value; } void InterpreterContext::messageBox(std::string_view message, const std::vector& buttons) { if (buttons.empty()) MWBase::Environment::get().getWindowManager()->messageBox(message); else MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); } void InterpreterContext::report(const std::string& message) {} int InterpreterContext::getGlobalShort(std::string_view name) const { return MWBase::Environment::get().getWorld()->getGlobalInt(name); } int InterpreterContext::getGlobalLong(std::string_view name) const { // a global long is internally a float. return MWBase::Environment::get().getWorld()->getGlobalInt(name); } float InterpreterContext::getGlobalFloat(std::string_view name) const { return MWBase::Environment::get().getWorld()->getGlobalFloat(name); } void InterpreterContext::setGlobalShort(std::string_view name, int value) { MWBase::Environment::get().getWorld()->setGlobalInt(name, value); } void InterpreterContext::setGlobalLong(std::string_view name, int value) { MWBase::Environment::get().getWorld()->setGlobalInt(name, value); } void InterpreterContext::setGlobalFloat(std::string_view name, float value) { MWBase::Environment::get().getWorld()->setGlobalFloat(name, value); } std::vector InterpreterContext::getGlobals() const { const MWWorld::Store& globals = MWBase::Environment::get().getESMStore()->get(); std::vector ids; for (const auto& globalVariable : globals) { ids.emplace_back(globalVariable.mId.getRefIdString()); } return ids; } char InterpreterContext::getGlobalType(std::string_view name) const { MWBase::World* world = MWBase::Environment::get().getWorld(); return world->getGlobalVariableType(name); } std::string InterpreterContext::getActionBinding(std::string_view targetAction) const { MWBase::InputManager* input = MWBase::Environment::get().getInputManager(); const auto& actions = input->getActionKeySorting(); for (const int action : actions) { std::string_view desc = input->getActionDescription(action); if (desc.empty()) continue; if (desc == targetAction) { if (input->joystickLastUsed()) return input->getActionControllerBindingName(action); else return input->getActionKeyBindingName(action); } } return "None"; } std::string_view InterpreterContext::getActorName() const { const MWWorld::Ptr& ptr = getReferenceImp(); if (ptr.getClass().isNpc()) { const ESM::NPC* npc = ptr.get()->mBase; return npc->mName; } const ESM::Creature* creature = ptr.get()->mBase; return creature->mName; } std::string_view InterpreterContext::getNPCRace() const { const ESM::NPC* npc = getReferenceImp().get()->mBase; const ESM::Race* race = MWBase::Environment::get().getESMStore()->get().find(npc->mRace); return race->mName; } std::string_view InterpreterContext::getNPCClass() const { const ESM::NPC* npc = getReferenceImp().get()->mBase; const ESM::Class* class_ = MWBase::Environment::get().getESMStore()->get().find(npc->mClass); return class_->mName; } std::string_view InterpreterContext::getNPCFaction() const { const ESM::NPC* npc = getReferenceImp().get()->mBase; const ESM::Faction* faction = MWBase::Environment::get().getESMStore()->get().find(npc->mFaction); return faction->mName; } std::string_view InterpreterContext::getNPCRank() const { const MWWorld::Ptr& ptr = getReferenceImp(); const ESM::RefId& faction = ptr.getClass().getPrimaryFaction(ptr); if (faction.empty()) throw std::runtime_error("getNPCRank(): NPC is not in a faction"); int rank = ptr.getClass().getPrimaryFactionRank(ptr); if (rank < 0 || rank > 9) throw std::runtime_error("getNPCRank(): invalid rank"); 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_view InterpreterContext::getPCName() const { MWBase::World* world = MWBase::Environment::get().getWorld(); return world->getPlayerPtr().get()->mBase->mName; } std::string_view InterpreterContext::getPCRace() const { 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_view InterpreterContext::getPCClass() const { 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_view InterpreterContext::getPCRank() const { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCRank(): NPC is not in a faction"); const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); auto it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) rank = it->second; // If you are not in the faction, PcRank returns the first rank, for whatever reason. // This is used by the dialogue when joining the Thieves Guild in Balmora. if (rank == -1) rank = 0; 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 {}; return faction->mRanks[rank]; } std::string_view InterpreterContext::getPCNextRank() const { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); const ESM::RefId& factionId = getReferenceImp().getClass().getPrimaryFaction(getReferenceImp()); if (factionId.empty()) throw std::runtime_error("getPCNextRank(): NPC is not in a faction"); const auto& ranks = player.getClass().getNpcStats(player).getFactionRanks(); auto it = ranks.find(factionId); int rank = -1; if (it != ranks.end()) rank = it->second; ++rank; // Next rank // if we are already at max rank, there is no next rank if (rank > 9) rank = 9; const MWWorld::ESMStore& store = world->getStore(); const ESM::Faction* faction = store.get().find(factionId); if (rank < 0) return {}; return faction->mRanks[rank]; } int InterpreterContext::getPCBounty() const { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world->getPlayerPtr(); return player.getClass().getNpcStats(player).getBounty(); } std::string_view InterpreterContext::getCurrentCellName() const { return MWBase::Environment::get().getWorld()->getCellName(); } void InterpreterContext::executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& 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(ESM::RefId id, std::string_view name, bool global) const { const Locals& locals = getMemberLocals(global, id); return locals.mShorts[findLocalVariableIndex(id, name, 's')]; } int InterpreterContext::getMemberLong(ESM::RefId id, std::string_view name, bool global) const { const Locals& locals = getMemberLocals(global, id); return locals.mLongs[findLocalVariableIndex(id, name, 'l')]; } float InterpreterContext::getMemberFloat(ESM::RefId id, std::string_view name, bool global) const { const Locals& locals = getMemberLocals(global, id); return locals.mFloats[findLocalVariableIndex(id, name, 'f')]; } void InterpreterContext::setMemberShort(ESM::RefId id, std::string_view name, int value, bool global) { Locals& locals = getMemberLocals(global, id); locals.mShorts[findLocalVariableIndex(id, name, 's')] = value; } void InterpreterContext::setMemberLong(ESM::RefId id, std::string_view name, int value, bool global) { Locals& locals = getMemberLocals(global, id); locals.mLongs[findLocalVariableIndex(id, name, 'l')] = value; } void InterpreterContext::setMemberFloat(ESM::RefId id, std::string_view name, float value, bool global) { Locals& locals = getMemberLocals(global, id); locals.mFloats[findLocalVariableIndex(id, name, 'f')] = value; } MWWorld::Ptr InterpreterContext::getReference(bool required) const { return getReferenceImp({}, true, required); } void InterpreterContext::updatePtr(const MWWorld::Ptr& base, const MWWorld::Ptr& updated) { if (!mReference.isEmpty() && base == mReference) { mReference = updated; if (mLocals == &base.getRefData().getLocals()) mLocals = &mReference.getRefData().getLocals(); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/interpretercontext.hpp000066400000000000000000000112731503074453300252250ustar00rootroot00000000000000#ifndef GAME_SCRIPT_INTERPRETERCONTEXT_H #define GAME_SCRIPT_INTERPRETERCONTEXT_H #include #include #include #include #include "globalscripts.hpp" #include "../mwworld/ptr.hpp" namespace MWScript { class Locals; class MissingImplicitRefError : public std::runtime_error { public: MissingImplicitRefError(); }; class InterpreterContext : public Interpreter::Context { 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 ESM::RefId& id = ESM::RefId(), bool activeOnly = false, bool doThrow = true) const; 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(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 ESM::RefId& scriptId, std::string_view name, char type) const; 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. ESM::RefId getTarget() const override; int getLocalShort(int index) const override; int getLocalLong(int index) const override; float getLocalFloat(int index) const override; void setLocalShort(int index, int value) override; void setLocalLong(int index, int value) override; void setLocalFloat(int index, float value) override; using Interpreter::Context::messageBox; void messageBox(std::string_view message, const std::vector& buttons) override; void report(const std::string& message) override; ///< By default, do nothing. int getGlobalShort(std::string_view name) const override; int getGlobalLong(std::string_view name) const override; float getGlobalFloat(std::string_view name) const override; void setGlobalShort(std::string_view name, int value) override; void setGlobalLong(std::string_view name, int value) override; void setGlobalFloat(std::string_view name, float value) override; std::vector getGlobals() const override; char getGlobalType(std::string_view name) const override; std::string getActionBinding(std::string_view action) const override; std::string_view getActorName() const override; std::string_view getNPCRace() const override; std::string_view getNPCClass() const override; std::string_view getNPCFaction() const override; std::string_view getNPCRank() const override; std::string_view getPCName() const override; std::string_view getPCRace() const override; std::string_view getPCClass() const override; std::string_view getPCRank() const override; std::string_view getPCNextRank() const override; int getPCBounty() const override; std::string_view getCurrentCellName() 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 getMemberShort(ESM::RefId id, std::string_view name, bool global) const override; int getMemberLong(ESM::RefId id, std::string_view name, bool global) const override; float getMemberFloat(ESM::RefId id, std::string_view name, bool global) const override; void setMemberShort(ESM::RefId id, std::string_view name, int value, bool global) override; void setMemberLong(ESM::RefId id, std::string_view name, int value, bool global) override; void setMemberFloat(ESM::RefId id, std::string_view name, float value, bool global) override; 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. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/locals.cpp000066400000000000000000000174111503074453300225250ustar00rootroot00000000000000#include "locals.hpp" #include "globalscripts.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/scriptmanager.hpp" #include "../mwworld/esmstore.hpp" namespace MWScript { void Locals::ensure(const ESM::RefId& scriptName) { if (!mInitialised) { const ESM::Script* script = MWBase::Environment::get().getESMStore()->get().find(scriptName); configure(*script); } } Locals::Locals() : mInitialised(false) { } bool Locals::configure(const ESM::Script& script) { if (mInitialised) return false; const GlobalScriptDesc* global = MWBase::Environment::get().getScriptManager()->getGlobalScripts().getScriptIfPresent(script.mId); if (global) { mShorts = global->mLocals.mShorts; mLongs = global->mLocals.mLongs; mFloats = global->mLocals.mFloats; } else { const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script.mId); mShorts.clear(); mShorts.resize(locals.get('s').size(), 0); mLongs.clear(); mLongs.resize(locals.get('l').size(), 0); mFloats.clear(); mFloats.resize(locals.get('f').size(), 0); } mScriptId = script.mId; mInitialised = true; return true; } bool Locals::isEmpty() const { return (mShorts.empty() && mLongs.empty() && mFloats.empty()); } bool Locals::hasVar(const ESM::RefId& script, std::string_view var) { ensure(script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); return (index != -1); } double Locals::getVarAsDouble(const ESM::RefId& script, std::string_view var) { ensure(script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); if (index == -1) return 0; switch (locals.getType(var)) { case 's': return mShorts.at(index); case 'l': return mLongs.at(index); case 'f': return mFloats.at(index); default: return 0; } } bool Locals::setVar(const ESM::RefId& script, std::string_view var, double val) { ensure(script); const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = locals.getIndex(var); if (index == -1) return false; switch (locals.getType(var)) { case 's': mShorts.at(index) = static_cast(val); break; case 'l': mLongs.at(index) = static_cast(val); break; case 'f': mFloats.at(index) = static_cast(val); break; } return true; } std::size_t Locals::getSize(const ESM::RefId& script) { ensure(script); return mShorts.size() + mLongs.size() + mFloats.size(); } bool Locals::write(ESM::Locals& locals, const ESM::RefId& script) const { if (!mInitialised) return false; const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); for (int i = 0; i < 3; ++i) { char type = 0; switch (i) { case 0: type = 's'; break; case 1: type = 'l'; break; case 2: type = 'f'; break; } const std::vector& names = declarations.get(type); for (int i2 = 0; i2 < static_cast(names.size()); ++i2) { 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); } } return true; } void Locals::read(const ESM::Locals& locals, const ESM::RefId& script) { ensure(script); const Compiler::Locals& declarations = MWBase::Environment::get().getScriptManager()->getLocals(script); int index = 0, numshorts = 0, numlongs = 0; for (unsigned int v = 0; v < locals.mVariables.size(); ++v) { ESM::VarType type = locals.mVariables[v].second.getType(); if (type == ESM::VT_Short) ++numshorts; else if (type == ESM::VT_Int) ++numlongs; } for (std::vector>::const_iterator iter = locals.mVariables.begin(); iter != locals.mVariables.end(); ++iter, ++index) { if (iter->first.empty()) { // 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(); } } 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; 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 (...) { // ignore type changes /// \todo write to log } } } } } openmw-openmw-0.49.0/apps/openmw/mwscript/locals.hpp000066400000000000000000000050701503074453300225300ustar00rootroot00000000000000#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; 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/miscextensions.cpp000066400000000000000000002316531503074453300243310ustar00rootroot00000000000000#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 "../mwbase/environment.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/containerstore.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" #include "../mwworld/manualref.hpp" #include "../mwworld/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/aicast.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwrender/animation.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace { 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) { if (levelItem.mLevel == level && itemId == levelItem.mId) return; } ESM::LevelledListBase::LevelItem item; item.mId = itemId; item.mLevel = level; list->mList.push_back(item); } 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();) { if (level != -1 && it->mLevel != level) { ++it; continue; } if (itemId == it->mId) it = list->mList.erase(it); else ++it; } } } namespace MWScript { namespace Misc { class OpMenuMode : public Interpreter::Opcode0 { 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(); if (limit < 0) throw std::runtime_error("random: argument out of range (Don't be so negative!)"); auto& prng = MWBase::Environment::get().getWorld()->getPrng(); runtime.push(static_cast(::Misc::Rng::rollDice(limit, prng))); // [o, limit) } }; template class OpStartScript : public Interpreter::Opcode0 { 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(); if (!MWBase::Environment::get().getESMStore()->get().search(name)) { 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 { 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: void execute(Interpreter::Runtime& runtime) override { const ESM::RefId& name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (!MWBase::Environment::get().getESMStore()->get().search(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()); } }; template class OpEnable : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWBase::Environment::get().getWorld()->enable(ptr); } }; template class OpDisable : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr; if (!R::implicit) { ESM::RefId name = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ptr = MWBase::Environment::get().getWorld()->searchPtr(name, false); // We don't normally want to let this go, but some mods insist on trying this if (ptr.isEmpty()) { const std::string error = "Failed to find an instance of object " + name.toDebugString(); runtime.getContext().report(error); Log(Debug::Error) << error; return; } } else { ptr = R()(runtime); } MWBase::Environment::get().getWorld()->disable(ptr); } }; template class OpGetDisabled : public Interpreter::Opcode0 { 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 { 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); } }; class OpGetPcSleep : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { runtime.push(MWBase::Environment::get().getWindowManager()->getPlayerSleeping()); } }; class OpGetPcJumping : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); runtime.push(world->getPlayer().getJumping()); } }; class OpWakeUpPc : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWindowManager()->wakeUpPlayer(); } }; class OpXBox : public Interpreter::Opcode0 { 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); runtime.push(ptr.getRefData().onActivate()); } }; template class OpActivate : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { InterpreterContext& context = static_cast(runtime.getContext()); MWWorld::Ptr ptr = R()(runtime); if (ptr.getRefData().activateByScript() || ptr.getContainerStore()) context.executeActivation(ptr, MWMechanics::getPlayer()); } }; template class OpLock : public Interpreter::Opcode1 { 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; } if (arg0 == 1) { lockLevel = runtime[0].mInteger; runtime.pop(); } 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.getType() == ESM::Door::sRecordId && !ptr.getCellRef().getTeleport()) { MWBase::Environment::get().getWorld()->activateDoor(ptr, MWWorld::DoorState::Idle); } } }; template class OpUnlock : public Interpreter::Opcode0 { 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); 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); 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); 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(); runtime.getContext().report(enabled ? "Border Rendering -> On" : "Border Rendering -> Off"); } }; class OpTogglePathgrid : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleRenderMode(MWRender::Render_Pathgrid); 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(); 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(); 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(); Interpreter::Type_Float time = runtime[0].mFloat; runtime.pop(); 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"); } }; class OpToggleWorld : public Interpreter::Opcode0 { 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. } }; class OpPcForce1stPerson : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { if (!MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); } }; class OpPcForce3rdPerson : public Interpreter::Opcode0 { void execute(Interpreter::Runtime& runtime) override { if (MWBase::Environment::get().getWorld()->isFirstPerson()) MWBase::Environment::get().getWorld()->togglePOV(true); } }; class OpPcGet3rdPerson : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { runtime.push(!MWBase::Environment::get().getWorld()->isFirstPerson()); } }; class OpToggleVanityMode : public Interpreter::Opcode0 { static bool sActivate; public: void execute(Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); if (world->toggleVanityMode(sActivate)) { runtime.getContext().report(sActivate ? "Vanity Mode -> On" : "Vanity Mode -> Off"); sActivate = !sActivate; } else { runtime.getContext().report("Vanity Mode -> No"); } } }; bool OpToggleVanityMode::sActivate = true; template class OpGetLocked : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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_view effect = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!ptr.getClass().isActor()) { runtime.push(0); return; } long key; if (const auto k = ::Misc::StringUtils::toNumeric(effect.data()); k.has_value() && *k >= 0 && *k <= 32767) key = *k; else key = ESM::MagicEffect::effectGmstIdToIndex(effect); const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); for (const auto& spell : stats.getActiveSpells()) { for (const auto& effect : spell.getEffects()) { if (effect.mFlags & ESM::ActiveEffect::Flag_Applied && effect.mEffectId == key) { runtime.push(1); return; } } } runtime.push(0); } }; template class OpAddSoulGem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId creature = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ESM::RefId gem = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (!ptr.getClass().isActor()) return; 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); // 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); } }; template class OpRemoveSoulGem : public Interpreter::Opcode1 { public: 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(); // throw away additional arguments for (unsigned int i = 0; i < arg0; ++i) runtime.pop(); if (!ptr.getClass().isActor()) return; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (it->getCellRef().getSoul() == soul) { store.remove(*it, 1); return; } } } }; template class OpDrop : public Interpreter::Opcode0 { 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 amount = runtime[0].mInteger; runtime.pop(); if (amount < 0) throw std::runtime_error("amount must be non-negative"); // no-op if (amount == 0) return; if (!ptr.getClass().isActor()) return; 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) { MWWorld::ConstContainerStoreIterator it = invStorePtr->getSlot(slot); if (it != invStorePtr->end() && it->getCellRef().getRefId() == item) { numNotEquipped -= it->getCellRef().getCount(); } } 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) { int numToRemove = std::min(amount - numNotEquipped, it->getCellRef().getCount()); invStorePtr->unequipItemQuantity(*it, numToRemove); numNotEquipped += numToRemove; } } } 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; if (amount <= 0) break; } } MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), item, 1); MWWorld::Ptr itemPtr(ref.getPtr()); if (amount > 0) { 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().getSoundManager()->playSound3D( ptr, itemPtr.getClass().getDownSoundId(itemPtr), 1.f, 1.f); } }; template class OpDropSoulGem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId soul = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (!ptr.getClass().isActor()) return; MWWorld::ContainerStore& store = ptr.getClass().getContainerStore(ptr); for (MWWorld::ContainerStoreIterator iter(store.begin()); iter != store.end(); ++iter) { if (iter->getCellRef().getSoul() == soul) { 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); runtime.push(ptr.getClass().getCreatureStats(ptr).getAttacked()); } }; template class OpGetWeaponDrawn : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); auto& cls = ptr.getClass(); if (!cls.hasInventoryStore(ptr) && !cls.isBipedal(ptr)) { runtime.push(0); return; } 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); runtime.push(ptr.getClass().getCreatureStats(ptr).getDrawState() == MWMechanics::DrawState::Spell); } }; template class OpGetSpellEffects : public Interpreter::Opcode0 { 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(); if (!ptr.getClass().isActor()) { 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 { runtime.push(MWBase::Environment::get().getWorld()->getTimeStamp().getHour()); } }; 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(); 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: 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)); } }; template class OpFall : public Interpreter::Opcode0 { 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)); } }; 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)); } }; 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)); } }; 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)); } }; 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(); 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(); 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()); } }; template class OpHitOnMe : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId objectID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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); ESM::RefId objectID = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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); } }; template class OpEnableLevitation : public Interpreter::Opcode0 { 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 { MWWorld::Ptr ptr = R()(runtime, false); std::string_view var = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); std::stringstream output; if (!ptr.isEmpty()) { ESM::RefId script = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); if (!script.empty()) { const Compiler::Locals& locals = MWBase::Environment::get().getScriptManager()->getLocals(script); char type = locals.getType(var); 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; } } } if (output.rdbuf()->in_avail() == 0) { 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"; } } runtime.getContext().report(output.str()); } }; template class OpShowVars : public Interpreter::Opcode0 { void printLocalVars(Interpreter::Runtime& runtime, const MWWorld::Ptr& ptr) { std::stringstream str; 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 " << ptr.getCellRef().getRefId(); 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) { if (i >= locals.mShorts.size()) break; str << std::endl << " " << (*names)[i] << " = " << locals.mShorts[i] << " (short)"; } names = &complocals.get('l'); for (size_t i = 0; i < names->size(); ++i) { if (i >= locals.mLongs.size()) break; str << std::endl << " " << (*names)[i] << " = " << locals.mLongs[i] << " (long)"; } names = &complocals.get('f'); for (size_t i = 0; i < names->size(); ++i) { if (i >= locals.mFloats.size()) break; str << std::endl << " " << (*names)[i] << " = " << locals.mFloats[i] << " (float)"; } } runtime.getContext().report(str.str()); } void printGlobalVars(Interpreter::Runtime& runtime) { std::stringstream str; str << "Global variables:"; MWBase::World* world = MWBase::Environment::get().getWorld(); std::vector names = runtime.getContext().getGlobals(); for (size_t i = 0; i < names.size(); ++i) { char type = world->getGlobalVariableType(names[i]); str << std::endl << " " << names[i] << " = "; switch (type) { case 's': str << runtime.getContext().getGlobalShort(names[i]) << " (short)"; break; case 'l': str << runtime.getContext().getGlobalLong(names[i]) << " (long)"; break; case 'f': str << runtime.getContext().getGlobalFloat(names[i]) << " (float)"; break; default: str << ""; } } runtime.getContext().report(str.str()); } public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); if (!ptr.isEmpty()) printLocalVars(runtime, ptr); else { // No reference, no problem. printGlobalVars(runtime); } } }; class OpToggleScripts : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleScripts(); runtime.getContext().report(enabled ? "Scripts -> On" : "Scripts -> Off"); } }; class OpToggleGodMode : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleGodMode(); runtime.getContext().report(enabled ? "God Mode -> On" : "God Mode -> Off"); } }; template class OpCast : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId spellId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); ESM::RefId targetId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(spellId); if (!spell) { runtime.getContext().report( "spellcasting failed: cannot find spell \"" + spellId.getRefIdString() + "\""); return; } if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { if (!MWBase::Environment::get().getMechanicsManager()->isCastingSpell(ptr)) { MWMechanics::AiCast castPackage(targetId, spell->mId, true); ptr.getClass().getCreatureStats(ptr).getAiSequence().stack(castPackage, ptr); } return; } MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetId, false, false); if (target.isEmpty()) return; MWMechanics::CastSpell cast(ptr, target, false, true); cast.playSpellCastingEffects(spell); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); } }; template class OpExplodeSpell : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId spellId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().search(spellId); if (!spell) { runtime.getContext().report( "spellcasting failed: cannot find spell \"" + spellId.getRefIdString() + "\""); return; } if (ptr == MWMechanics::getPlayer()) { MWBase::Environment::get().getWorld()->getPlayer().setSelectedSpell(spell->mId); return; } if (ptr.getClass().isActor()) { 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; } MWMechanics::CastSpell cast(ptr, ptr, false, true); cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); } }; class OpGoToJail : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::World* world = MWBase::Environment::get().getWorld(); world->goToJail(); } }; class OpPayFine : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); 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 { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setBounty(0); MWBase::Environment::get().getWorld()->getPlayer().recordCrimeId(); } }; class OpGetPcInJail : public Interpreter::Opcode0 { 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()); } }; template class OpBetaComment : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime); std::stringstream msg; msg << "Report time: "; std::time_t currentTime = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); 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: " << ptr.getCellRef().getRefNum().mContentFile; if (!ptr.getCellRef().hasContentFile()) 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; if (ptr.getRefData().isDeletedByContentFile()) msg << "[Deleted by content file]" << std::endl; if (!ptr.getCellRef().getCount()) msg << "[Deleted]" << std::endl; msg << "RefID: " << ptr.getCellRef().getRefId() << std::endl; msg << "Memory address: " << ptr.getBase() << std::endl; if (ptr.isInCell()) { 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 << "Coordinates: " << pos.x() << " " << pos.y() << " " << pos.z() << std::endl; auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); 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()) 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; } while (arg0 > 0) { std::string_view notes = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); if (!notes.empty()) msg << "Notes: " << notes << std::endl; --arg0; } Log(Debug::Warning) << "\n" << msg.str(); runtime.getContext().report(msg.str()); } }; class OpAddToLevCreature : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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().getESMStore()->get().find(levId); addToLevList(&listCopy, creatureId, level); MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpRemoveFromLevCreature : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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().getESMStore()->get().find(levId); removeFromLevList(&listCopy, creatureId, level); MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpAddToLevItem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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().getESMStore()->get().find(levId); addToLevList(&listCopy, itemId, level); MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; class OpRemoveFromLevItem : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const ESM::RefId& levId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); 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().getESMStore()->get().find(levId); removeFromLevList(&listCopy, itemId, level); MWBase::Environment::get().getESMStore()->overrideRecord(listCopy); } }; template class OpShowSceneGraph : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::Ptr ptr = R()(runtime, false); int confirmed = 0; 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."); else { 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); 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); runtime.getContext().report(enabled ? "Agents Paths Rendering -> On" : "Agents Paths Rendering -> Off"); } }; class OpSetNavMeshNumberToRender : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { 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)); } }; template class OpRepairedOnMe : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { // Broken in vanilla and deliberately no-op. runtime.push(0); } }; class OpToggleRecastMesh : public Interpreter::Opcode0 { public: 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"); } }; 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); 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/miscextensions.hpp000066400000000000000000000004551503074453300243300ustar00rootroot00000000000000#ifndef GAME_SCRIPT_MISCEXTENSIONS_H #define GAME_SCRIPT_MISCEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Misc { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/ref.cpp000066400000000000000000000015401503074453300220200ustar00rootroot00000000000000#include "ref.hpp" #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "interpretercontext.hpp" MWWorld::Ptr MWScript::ExplicitRef::operator()(Interpreter::Runtime& runtime, bool required, bool activeOnly) const { ESM::RefId id = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (required) return MWBase::Environment::get().getWorld()->getPtr(id, activeOnly); else return MWBase::Environment::get().getWorld()->searchPtr(id, activeOnly); } MWWorld::Ptr MWScript::ImplicitRef::operator()(Interpreter::Runtime& runtime, bool required, bool activeOnly) const { MWScript::InterpreterContext& context = static_cast(runtime.getContext()); return context.getReference(required); } openmw-openmw-0.49.0/apps/openmw/mwscript/ref.hpp000066400000000000000000000010711503074453300220240ustar00rootroot00000000000000#ifndef GAME_MWSCRIPT_REF_H #define GAME_MWSCRIPT_REF_H #include "../mwworld/ptr.hpp" namespace Interpreter { class Runtime; } namespace MWScript { struct ExplicitRef { static constexpr bool implicit = false; 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/scriptmanagerimp.cpp000066400000000000000000000136171503074453300246210ustar00rootroot00000000000000#include "scriptmanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwworld/esmstore.hpp" #include "extensions.hpp" #include "interpretercontext.hpp" namespace MWScript { ScriptManager::ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode) : mErrorHandler() , mStore(store) , mCompilerContext(compilerContext) , mParser(mErrorHandler, mCompilerContext) , mGlobalScripts(store) { installOpcodes(mInterpreter); mErrorHandler.setWarningsMode(warningsMode); } bool ScriptManager::compile(const ESM::RefId& name) { mParser.reset(); mErrorHandler.reset(); if (const ESM::Script* script = mStore.get().find(name)) { mErrorHandler.setContext(script->mId.getRefIdString()); bool Success = true; try { std::istringstream input(script->mScriptText); Compiler::Scanner scanner(mErrorHandler, input, mCompilerContext.getExtensions()); scanner.scan(mParser); if (!mErrorHandler.isGood()) Success = false; } catch (const Compiler::SourceException&) { // error has already been reported via error handler Success = false; } catch (const std::exception& error) { Log(Debug::Error) << "Error: An exception has been thrown: " << error.what(); Success = false; } if (!Success) { Log(Debug::Error) << "Error: script compiling failed: " << name; } if (Success) { mScripts.emplace(name, CompiledScript(mParser.getProgram(), mParser.getLocals())); return true; } } return false; } bool ScriptManager::run(const ESM::RefId& name, Interpreter::Context& interpreterContext) { // compile script auto iter = mScripts.find(name); if (iter == mScripts.end()) { if (!compile(name)) { // failed -> ignore script from now on. mScripts.emplace(name, CompiledScript({}, Compiler::Locals())); return false; } iter = mScripts.find(name); assert(iter != mScripts.end()); } // execute script const auto& target = interpreterContext.getTarget(); if (!iter->second.mProgram.mInstructions.empty() && iter->second.mInactive.find(target) == iter->second.mInactive.end()) { try { mInterpreter.run(iter->second.mProgram, interpreterContext); return true; } catch (const MissingImplicitRefError& e) { 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(); iter->second.mInactive.insert(target); // don't execute again. } } return false; } void ScriptManager::clear() { for (auto& script : mScripts) { script.second.mInactive.clear(); } mGlobalScripts.clear(); } std::pair ScriptManager::compileAll() { int count = 0; int success = 0; for (auto& script : mStore.get()) { ++count; if (compile(script.mId)) ++success; } return std::make_pair(count, success); } const Compiler::Locals& ScriptManager::getLocals(const ESM::RefId& name) { { auto iter = mScripts.find(name); if (iter != mScripts.end()) return iter->second.mLocals; } { auto iter = mOtherLocals.find(name); if (iter != mOtherLocals.end()) return iter->second; } if (const ESM::Script* script = mStore.get().search(name)) { Compiler::Locals locals; 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()); 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(); } auto iter = mOtherLocals.emplace(name, locals).first; return iter->second; } throw std::logic_error("script " + name.toDebugString() + " does not exist"); } GlobalScripts& ScriptManager::getGlobalScripts() { return mGlobalScripts; } const Compiler::Extensions& ScriptManager::getExtensions() const { return *mCompilerContext.getExtensions(); } } openmw-openmw-0.49.0/apps/openmw/mwscript/scriptmanagerimp.hpp000066400000000000000000000043711503074453300246230ustar00rootroot00000000000000#ifndef GAME_SCRIPT_SCRIPTMANAGER_H #define GAME_SCRIPT_SCRIPTMANAGER_H #include #include #include #include #include #include #include #include #include "../mwbase/scriptmanager.hpp" #include "globalscripts.hpp" namespace MWWorld { class ESMStore; } namespace Compiler { class Context; } namespace Interpreter { class Context; class Interpreter; } namespace MWScript { class ScriptManager : public MWBase::ScriptManager { 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::unordered_map mScripts; GlobalScripts mGlobalScripts; std::unordered_map mOtherLocals; public: ScriptManager(const MWWorld::ESMStore& store, Compiler::Context& compilerContext, int warningsMode); 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 compile(const ESM::RefId& name) override; ///< Compile script with the given namen /// \return 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. GlobalScripts& getGlobalScripts() override; const Compiler::Extensions& getExtensions() const override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/skyextensions.cpp000066400000000000000000000107431503074453300241770ustar00rootroot00000000000000#include "skyextensions.hpp" #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.hpp" namespace MWScript { namespace Sky { class OpToggleSky : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { bool enabled = MWBase::Environment::get().getWorld()->toggleSky(); 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); } }; class OpTurnMoonRed : public Interpreter::Opcode0 { 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()); } }; class OpGetSecundaPhase : public Interpreter::Opcode0 { 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()); } }; class OpChangeWeather : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { ESM::RefId region = ESM::RefId::stringRefId(runtime.getStringLiteral(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 { std::string_view region{ runtime.getStringLiteral(runtime[0].mInteger) }; runtime.pop(); std::vector chances; chances.reserve(10); while (arg0 > 0) { chances.push_back(std::clamp(runtime[0].mInteger, 0, 100)); runtime.pop(); arg0--; } MWBase::Environment::get().getWorld()->modRegion(ESM::RefId::stringRefId(region), chances); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/skyextensions.hpp000066400000000000000000000005321503074453300241770ustar00rootroot00000000000000#ifndef GAME_SCRIPT_SKYEXTENSIONS_H #define GAME_SCRIPT_SKYEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief sky-related script functionality namespace Sky { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/soundextensions.cpp000066400000000000000000000215241503074453300245200ustar00rootroot00000000000000#include "soundextensions.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" #include "interpretercontext.hpp" #include "ref.hpp" namespace MWScript { namespace Sound { template class OpSay : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); MWScript::InterpreterContext& context = static_cast(runtime.getContext()); VFS::Path::Normalized file{ runtime.getStringLiteral(runtime[0].mInteger) }; runtime.pop(); std::string_view text = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); MWBase::Environment::get().getSoundManager()->say(ptr, Misc::ResourceHelpers::correctSoundPath(file)); if (Settings::gui().mSubtitles) context.messageBox(text); } }; template class OpSayDone : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(MWBase::Environment::get().getSoundManager()->sayDone(ptr)); } }; class OpStreamMusic : public Interpreter::Opcode0 { 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 { 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 { 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 pitch = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getSoundManager()->playSound( sound, volume, pitch, MWSound::Type::Sfx, MWSound::PlayMode::NoEnv); } }; template class OpPlaySound3D : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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, TLoop ? MWSound::PlayMode::LoopRemoveAtDistance : MWSound::PlayMode::Normal); } }; template class OpPlaySoundVP3D : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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 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 class OpStopSound : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId sound = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); MWBase::Environment::get().getSoundManager()->stopSound3D(ptr, sound); } }; template class OpGetSoundPlaying : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime, false); int index = runtime[0].mInteger; runtime.pop(); if (ptr.isEmpty()) { runtime.push(0); return; } bool ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying( ptr, ESM::RefId::stringRefId(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); if (!cont.isEmpty() && cont.getClass().hasInventoryStore(cont) && cont.getClass().getInventoryStore(cont).isEquipped(ptr)) { ret = MWBase::Environment::get().getSoundManager()->getSoundPlaying( cont, ESM::RefId::stringRefId(runtime.getStringLiteral(index))); } } runtime.push(ret); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/soundextensions.hpp000066400000000000000000000005361503074453300245250ustar00rootroot00000000000000#ifndef GAME_SCRIPT_SOUNDEXTENSIONS_H #define GAME_SCRIPT_SOUNDEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { namespace Sound { // Script-extensions related to sound void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/statsextensions.cpp000066400000000000000000001747351503074453300245430ustar00rootroot00000000000000#include "statsextensions.hpp" #include #include #include #include "../mwworld/esmstore.hpp" #include #include #include #include #include #include #include #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/player.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/npcstats.hpp" #include "ref.hpp" namespace { ESM::RefId getDialogueActorFaction(const MWWorld::ConstPtr& 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)"); 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 class OpGetLevel : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = -1; if (ptr.getClass().isActor()) value = ptr.getClass().getCreatureStats(ptr).getLevel(); runtime.push(value); } }; template class OpSetLevel : public Interpreter::Opcode0 { public: 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).setLevel(value); } }; template class OpGetAttribute : public Interpreter::Opcode0 { ESM::RefId mIndex; public: OpGetAttribute(ESM::RefId index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = 0.f; if (ptr.getClass().isActor()) value = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex).getModified(); runtime.push(value); } }; template class OpSetAttribute : public Interpreter::Opcode0 { ESM::RefId mIndex; public: OpSetAttribute(ESM::RefId index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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, true); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; template class OpModAttribute : public Interpreter::Opcode0 { ESM::RefId mIndex; public: OpModAttribute(ESM::RefId index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); if (!ptr.getClass().isActor()) return; MWMechanics::AttributeValue attribute = ptr.getClass().getCreatureStats(ptr).getAttribute(mIndex); modStat(attribute, value); ptr.getClass().getCreatureStats(ptr).setAttribute(mIndex, attribute); } }; template class OpGetDynamic : public Interpreter::Opcode0 { int mIndex; public: OpGetDynamic(int index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = 0.f; if (mIndex == 0 && ptr.getClass().hasItemHealth(ptr)) { // 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 class OpSetDynamic : public Interpreter::Opcode0 { int mIndex; public: OpSetDynamic(int index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); if (!ptr.getClass().isActor()) return; MWMechanics::DynamicStat stat(ptr.getClass().getCreatureStats(ptr).getDynamic(mIndex)); stat.setBase(value); stat.setCurrent(stat.getModified(false), true, true); ptr.getClass().getCreatureStats(ptr).setDynamic(mIndex, stat); } }; template class OpModDynamic : public Interpreter::Opcode0 { int mIndex; public: OpModDynamic(int index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { int peek = R::implicit ? 0 : runtime[0].mInteger; MWWorld::Ptr ptr = R()(runtime); 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 && 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"; return; } } MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); MWMechanics::DynamicStat stat = stats.getDynamic(mIndex); 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); stats.setDynamic(mIndex, stat); } }; template class OpModCurrentDynamic : public Interpreter::Opcode0 { int mIndex; public: OpModCurrentDynamic(int index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float diff = runtime[0].mFloat; runtime.pop(); if (!ptr.getClass().isActor()) 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)); 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 class OpGetDynamicGetRatio : public Interpreter::Opcode0 { int mIndex; public: OpGetDynamicGetRatio(int index) : mIndex(index) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getClass().isActor()) { runtime.push(0.f); return; } const MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); runtime.push(stats.getDynamic(mIndex).getRatio(false)); } }; template class OpGetSkill : public Interpreter::Opcode0 { ESM::RefId mId; public: OpGetSkill(ESM::RefId id) : mId(id) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getClass().isActor()) { runtime.push(0.f); return; } Interpreter::Type_Float value = ptr.getClass().getSkill(ptr, mId); runtime.push(value); } }; template class OpSetSkill : public Interpreter::Opcode0 { ESM::RefId mId; public: OpSetSkill(ESM::RefId id) : mId(id) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); if (!ptr.getClass().isNpc()) return; MWMechanics::NpcStats& stats = ptr.getClass().getNpcStats(ptr); stats.getSkill(mId).setBase(value, true); } }; template class OpModSkill : public Interpreter::Opcode0 { ESM::RefId mId; public: OpModSkill(ESM::RefId id) : mId(id) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float value = runtime[0].mFloat; runtime.pop(); if (!ptr.getClass().isNpc()) return; 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())); } }; class OpSetPCCrimeLevel : public Interpreter::Opcode0 { 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); 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(); 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 class OpAddSpell : public Interpreter::Opcode0 { 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(); if (!ptr.getClass().isActor()) return; const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get().find(id); 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 class OpRemoveSpell : public Interpreter::Opcode0 { 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(); if (!ptr.getClass().isActor()) return; MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); creatureStats.getSpells().remove(id); MWBase::WindowManager* wm = MWBase::Environment::get().getWindowManager(); if (ptr == MWMechanics::getPlayer() && id == wm->getSelectedSpell()) { wm->unsetSelectedSpell(); } } }; template class OpRemoveSpellEffects : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); ESM::RefId spellid = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid); } }; template class OpRemoveEffects : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer effectId = runtime[0].mInteger; runtime.pop(); if (ptr.getClass().isActor()) ptr.getClass().getCreatureStats(ptr).getActiveSpells().purgeEffect(ptr, effectId); } }; template class OpGetSpell : public Interpreter::Opcode0 { 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(); Interpreter::Type_Integer value = 0; if (ptr.getClass().isActor() && ptr.getClass().getCreatureStats(ptr).getSpells().hasSpell(id)) value = 1; runtime.push(value); } }; template class OpPCJoinFaction : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); ESM::RefId 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.empty()) { MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).joinFaction(factionID); } } }; template class OpPCRaiseRank : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); ESM::RefId 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.empty()) { MWWorld::Ptr player = MWMechanics::getPlayer(); if (!player.getClass().getNpcStats(player).isInFaction(factionID)) { player.getClass().getNpcStats(player).joinFaction(factionID); } else { int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID); player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank + 1); } } } }; template class OpPCLowerRank : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr actor = R()(runtime, false); ESM::RefId 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.empty()) { MWWorld::Ptr player = MWMechanics::getPlayer(); int currentRank = player.getClass().getNpcStats(player).getFactionRank(factionID); player.getClass().getNpcStats(player).setFactionRank(factionID, currentRank - 1); } } }; template class OpGetPCRank : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); ESM::RefId factionID; if (arg0 > 0) { 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(); runtime.push(player.getClass().getNpcStats(player).getFactionRank(factionID)); } else { runtime.push(-1); } } }; template class OpModDisposition : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); 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) } }; template class OpSetDisposition : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = runtime[0].mInteger; runtime.pop(); if (ptr.getClass().isNpc()) ptr.getClass().getNpcStats(ptr).setBaseDisposition(value); } }; template class OpGetDisposition : public Interpreter::Opcode0 { 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)); } }; class OpGetDeadCount : public Interpreter::Opcode0 { 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 class OpGetPCFacRep : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); ESM::RefId factionId; if (arg0 == 1) { factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) throw std::runtime_error("failed to determine faction"); MWWorld::Ptr player = MWMechanics::getPlayer(); runtime.push(player.getClass().getNpcStats(player).getFactionReputation(factionId)); } }; 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(); ESM::RefId factionId; if (arg0 == 1) { factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } if (factionId.empty()) throw std::runtime_error("failed to determine faction"); MWWorld::Ptr player = MWMechanics::getPlayer(); player.getClass().getNpcStats(player).setFactionReputation(factionId, value); } }; 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(); ESM::RefId factionId; if (arg0 == 1) { factionId = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); } else { factionId = getDialogueActorFaction(ptr); } 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); } }; template class OpGetCommonDisease : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (ptr.getClass().isActor()) runtime.push(ptr.getClass().getCreatureStats(ptr).hasCommonDisease()); else runtime.push(0); } }; template class OpGetBlightDisease : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (ptr.getClass().isActor()) runtime.push(ptr.getClass().getCreatureStats(ptr).hasBlightDisease()); else runtime.push(0); } }; template class OpGetRace : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::ConstPtr ptr = R()(runtime); ESM::RefId race = ESM::RefId::stringRefId(runtime.getStringLiteral(runtime[0].mInteger)); runtime.pop(); if (ptr.getClass().isNpc()) { const ESM::RefId& npcRace = ptr.get()->mBase->mRace; 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(); runtime.push(ptr.getClass().getNpcStats(ptr).getWerewolfKills()); } }; template class OpPcExpelled : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); ESM::RefId factionID; if (arg0 > 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: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); ESM::RefId factionID; if (arg0 > 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()) { player.getClass().getNpcStats(player).expell(factionID, true); } } }; template class OpPcClearExpelled : public Interpreter::Opcode1 { public: void execute(Interpreter::Runtime& runtime, unsigned int arg0) override { MWWorld::ConstPtr ptr = R()(runtime, false); ESM::RefId factionID; if (arg0 > 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()) player.getClass().getNpcStats(player).clearExpelled(factionID); } }; template class OpRaiseRank : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); if (factionID.empty()) return; MWWorld::Ptr player = MWMechanics::getPlayer(); // 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).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); const ESM::RefId& factionID = ptr.getClass().getPrimaryFaction(ptr); if (factionID.empty()) return; MWWorld::Ptr player = MWMechanics::getPlayer(); // 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).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: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = 0; if (ptr.getClass().isActor()) { auto& stats = ptr.getClass().getCreatureStats(ptr); value = stats.hasDied(); if (value) stats.clearHasDied(); } runtime.push(value); } }; template class OpOnMurder : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Integer value = 0; if (ptr.getClass().isActor()) { auto& stats = ptr.getClass().getCreatureStats(ptr); value = stats.hasBeenMurdered(); if (value) stats.clearHasBeenMurdered(); } runtime.push(value); } }; template class OpOnKnockout : public Interpreter::Opcode0 { public: 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).getKnockedDownOneFrame(); runtime.push(value); } }; template class OpIsWerewolf : public Interpreter::Opcode0 { 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); 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); if (ptr.getClass().isNpc()) MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(ptr); } }; template class OpResurrect : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.getClass().isActor()) return; 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); windowManager->forceLootMode(ptr); } else { MWBase::Environment::get().getWorld()->removeContainerScripts(ptr); // resets runtime state such as inventory, stats and AI. does not reset position in the world ptr.getRefData().setCustomData(nullptr); } if (wasEnabled) MWBase::Environment::get().getWorld()->enable(ptr); } } }; template class OpGetStat : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { // dummy runtime.pop(); runtime.push(0); } }; template class OpGetMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpGetMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } 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.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude(); // GetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); int ret = static_cast(currentValue); runtime.push(ret); } }; template class OpSetMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpSetMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } 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.getOrDefault(mPositiveEffect).getMagnitude(); if (mNegativeEffect != -1) currentValue -= effects.getOrDefault(mNegativeEffect).getMagnitude(); // SetResist* should take in account elemental shields if (mPositiveEffect == ESM::MagicEffect::ResistFire) currentValue += effects.getOrDefault(ESM::MagicEffect::FireShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistShock) currentValue += effects.getOrDefault(ESM::MagicEffect::LightningShield).getMagnitude(); if (mPositiveEffect == ESM::MagicEffect::ResistFrost) currentValue += effects.getOrDefault(ESM::MagicEffect::FrostShield).getMagnitude(); effects.modifyBase(mPositiveEffect, (arg - static_cast(currentValue))); } }; template class OpModMagicEffect : public Interpreter::Opcode0 { int mPositiveEffect; int mNegativeEffect; public: OpModMagicEffect(int positiveEffect, int negativeEffect) : mPositiveEffect(positiveEffect) , mNegativeEffect(negativeEffect) { } void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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) { 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 }, { ESM::MagicEffect::ResistFire, ESM::MagicEffect::WeaknessToFire }, { ESM::MagicEffect::ResistFrost, ESM::MagicEffect::WeaknessToFrost }, { ESM::MagicEffect::ResistShock, ESM::MagicEffect::WeaknessToShock }, { ESM::MagicEffect::ResistCommonDisease, ESM::MagicEffect::WeaknessToCommonDisease }, { ESM::MagicEffect::ResistBlightDisease, ESM::MagicEffect::WeaknessToBlightDisease }, { ESM::MagicEffect::ResistCorprusDisease, ESM::MagicEffect::WeaknessToCorprusDisease }, { ESM::MagicEffect::ResistPoison, ESM::MagicEffect::WeaknessToPoison }, { ESM::MagicEffect::ResistParalysis, -1 }, { ESM::MagicEffect::ResistNormalWeapons, ESM::MagicEffect::WeaknessToNormalWeapons }, { ESM::MagicEffect::WaterBreathing, -1 }, { ESM::MagicEffect::Chameleon, -1 }, { ESM::MagicEffect::WaterWalking, -1 }, { ESM::MagicEffect::SwiftSwim, -1 }, { ESM::MagicEffect::Jump, -1 }, { ESM::MagicEffect::Levitate, -1 }, { ESM::MagicEffect::Shield, -1 }, { ESM::MagicEffect::Sound, -1 }, { ESM::MagicEffect::Silence, -1 }, { ESM::MagicEffect::Blind, -1 }, { ESM::MagicEffect::Paralyze, -1 }, { ESM::MagicEffect::Invisibility, -1 }, { ESM::MagicEffect::FortifyAttack, -1 }, { ESM::MagicEffect::Sanctuary, -1 }, }; for (int i = 0; i < 24; ++i) { int positive = sMagicEffects[i].mPositiveEffect; int negative = sMagicEffects[i].mNegativeEffect; interpreter.installSegment5>( Compiler::Stats::opcodeGetMagicEffect + i, positive, negative); interpreter.installSegment5>( Compiler::Stats::opcodeGetMagicEffectExplicit + i, positive, negative); interpreter.installSegment5>( Compiler::Stats::opcodeSetMagicEffect + i, positive, negative); interpreter.installSegment5>( Compiler::Stats::opcodeSetMagicEffectExplicit + i, 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/statsextensions.hpp000066400000000000000000000005671503074453300245370ustar00rootroot00000000000000#ifndef GAME_SCRIPT_STATSEXTENSIONS_H #define GAME_SCRIPT_STATSEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Stats { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/transformationextensions.cpp000066400000000000000000001103731503074453300264370ustar00rootroot00000000000000#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" namespace MWScript { namespace Transformation { void moveStandingActors(const MWWorld::Ptr& ptr, const osg::Vec3f& diff) { std::vector actors; MWBase::Environment::get().getWorld()->getActorsStandingOn(ptr, actors); for (auto& actor : actors) MWBase::Environment::get().getWorld()->moveObjectBy(actor, diff, false); } template class OpGetDistance : public Interpreter::Opcode0 { 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(); if (from.isEmpty()) { 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 { 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; } } 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; } 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]; 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]; distance = static_cast(std::sqrt(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2])); } runtime.push(distance); } }; template class OpSetScale : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); Interpreter::Type_Float scale = runtime[0].mFloat; runtime.pop(); MWBase::Environment::get().getWorld()->scaleObject(ptr, scale); } }; template class OpGetScale : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); runtime.push(ptr.getCellRef().getScale()); } }; template class OpModScale : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); 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); } }; template class OpSetAngle : public Interpreter::Opcode0 { 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 class OpGetStartingAngle : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); float ret = 0.f; if (!axis.empty()) { if (axis[0] == 'x') { ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[0]); } else if (axis[0] == 'y') { ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[1]); } else if (axis[0] == 'z') { ret = osg::RadiansToDegrees(ptr.getCellRef().getPosition().rot[2]); } } runtime.push(ret); } }; template class OpGetAngle : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); float ret = 0.f; if (!axis.empty()) { if (axis[0] == 'x') { ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[0]); } else if (axis[0] == 'y') { ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[1]); } else if (axis[0] == 'z') { ret = osg::RadiansToDegrees(ptr.getRefData().getPosition().rot[2]); } } runtime.push(ret); } }; template class OpGetPos : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); float ret = 0.f; if (!axis.empty()) { if (axis[0] == 'x') { ret = ptr.getRefData().getPosition().pos[0]; } else if (axis[0] == 'y') { ret = ptr.getRefData().getPosition().pos[1]; } else if (axis[0] == 'z') { ret = ptr.getRefData().getPosition().pos[2]; } } runtime.push(ret); } }; template class OpSetPos : public Interpreter::Opcode0 { 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 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. 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, ptr.getCell()->getCell()->getWorldSpace()); if (pos < terrainHeight) pos = terrainHeight; } newPos[2] = pos; } else { return; } dynamic_cast(runtime.getContext()) .updatePtr(ptr, MWBase::Environment::get().getWorld()->moveObjectBy(ptr, newPos - curPos, true)); } }; template class OpGetStartingPos : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); std::string_view axis = runtime.getStringLiteral(runtime[0].mInteger); runtime.pop(); float ret = 0.f; if (!axis.empty()) { if (axis[0] == 'x') { ret = ptr.getCellRef().getPosition().pos[0]; } else if (axis[0] == 'y') { ret = ptr.getCellRef().getPosition().pos[1]; } else if (axis[0] == 'z') { ret = ptr.getCellRef().getPosition().pos[2]; } } runtime.push(ret); } }; template class OpPositionCell : public Interpreter::Opcode0 { 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) { // 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) { 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 class OpPosition : public Interpreter::Opcode0 { 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::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 { 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) { 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 { 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(); MWWorld::Ptr player = MWMechanics::getPlayer(); if (!player.isInCell()) throw std::runtime_error("player not in a cell"); 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 class OpPlaceAt : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr actor = pc ? MWMechanics::getPlayer() : R()(runtime); 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(); if (direction < 0 || direction > 3) throw std::runtime_error("invalid direction"); 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"); 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()); } } }; template class OpRotate : public Interpreter::Opcode0 { 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 class OpRotateWorld : public Interpreter::Opcode0 { 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 class OpSetAtStart : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWWorld::Ptr ptr = R()(runtime); if (!ptr.isInCell()) return; 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 class OpMove : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { const 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 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; 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)); } }; template class OpMoveWorld : public Interpreter::Opcode0 { 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 { MWBase::Environment::get().getWorld()->resetActors(); } }; class OpFixme : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { MWBase::Environment::get().getWorld()->fixPosition(); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/transformationextensions.hpp000066400000000000000000000006221503074453300264370ustar00rootroot00000000000000#ifndef GAME_SCRIPT_TRANSFORMATIONEXTENSIONS_H #define GAME_SCRIPT_TRANSFORMATIONEXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief stats-related script functionality (creatures and NPCs) namespace Transformation { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwscript/userextensions.cpp000066400000000000000000000042311503074453300243420ustar00rootroot00000000000000#include "userextensions.hpp" #include #include #include #include #include #include "ref.hpp" namespace MWScript { /// Temporary script extensions. /// /// \attention Do not commit changes to this file to a git repository! namespace User { class OpUser1 : public Interpreter::Opcode0 { 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"); } }; template class OpUser3 : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { // MWWorld::Ptr ptr = R()(runtime); runtime.getContext().report("user3: not in use"); } }; template class OpUser4 : public Interpreter::Opcode0 { public: void execute(Interpreter::Runtime& runtime) override { // MWWorld::Ptr ptr = R()(runtime); runtime.getContext().report("user4: not in use"); } }; void installOpcodes(Interpreter::Interpreter& interpreter) { 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); } } } openmw-openmw-0.49.0/apps/openmw/mwscript/userextensions.hpp000066400000000000000000000005621503074453300243520ustar00rootroot00000000000000#ifndef GAME_SCRIPT_USEREXTENSIONS_H #define GAME_SCRIPT_USEREXTENSIONS_H namespace Compiler { class Extensions; } namespace Interpreter { class Interpreter; } namespace MWScript { /// \brief Temporary script functionality limited to the console namespace User { void installOpcodes(Interpreter::Interpreter& interpreter); } } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/000077500000000000000000000000001503074453300203645ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwsound/alext.h000066400000000000000000000553641503074453300216670ustar00rootroot00000000000000#ifndef AL_ALEXT_H #define AL_ALEXT_H #include /* 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 _alsoft_int64_t; typedef unsigned __int64 _alsoft_uint64_t; #else /* Fallback if nothing above works */ #include typedef int64_t _alsoft_int64_t; typedef uint64_t _alsoft_uint64_t; #endif #include "al.h" #include "alc.h" #ifdef __cplusplus 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 #endif #ifndef AL_LOKI_WAVE_format #define AL_LOKI_WAVE_format 1 #define AL_FORMAT_WAVE_EXT 0x10002 #endif #ifndef AL_EXT_vorbis #define AL_EXT_vorbis 1 #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 #endif #ifndef AL_EXT_float32 #define AL_EXT_float32 1 #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 #endif #ifndef AL_EXT_MULAW #define AL_EXT_MULAW 1 #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 #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 #endif #ifndef AL_EXT_MCFORMATS #define AL_EXT_MCFORMATS 1 /* 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 #endif #ifndef AL_EXT_IMA4 #define AL_EXT_IMA4 1 #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 void(AL_APIENTRY* PFNALBUFFERDATASTATICPROC)(const ALint, ALenum, ALvoid*, ALsizei, ALsizei); #ifdef AL_ALEXT_PROTOTYPES AL_API void AL_APIENTRY alBufferDataStatic( const ALint buffer, ALenum format, ALvoid* data, ALsizei len, ALsizei freq); #endif #endif #ifndef ALC_EXT_EFX #define ALC_EXT_EFX 1 #include "efx.h" #endif #ifndef ALC_EXT_disconnect #define ALC_EXT_disconnect 1 #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); #ifdef AL_ALEXT_PROTOTYPES 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 #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 void(AL_APIENTRY* PFNALBUFFERSUBDATASOFTPROC)(ALuint, ALenum, const ALvoid*, ALsizei, ALsizei); #ifdef AL_ALEXT_PROTOTYPES 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 #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); #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); #endif #endif #ifndef ALC_EXT_DEDICATED #define ALC_EXT_DEDICATED 1 #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 /* 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 /* 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 /* 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); #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); #endif #endif #ifndef AL_SOFT_direct_channels #define AL_SOFT_direct_channels 1 #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 /* 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 /* 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); #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); #endif #endif #ifndef AL_EXT_STEREO_ANGLES #define AL_EXT_STEREO_ANGLES 1 #define AL_STEREO_ANGLES 0x1030 #endif #ifndef AL_EXT_SOURCE_RADIUS #define AL_EXT_SOURCE_RADIUS 1 #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 _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); #endif #endif #ifndef ALC_EXT_DEFAULT_FILTER_ORDER #define ALC_EXT_DEFAULT_FILTER_ORDER 1 #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 void(AL_APIENTRY* LPALDEFERUPDATESSOFT)(void); typedef void(AL_APIENTRY* LPALPROCESSUPDATESSOFT)(void); #ifdef AL_ALEXT_PROTOTYPES 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 #endif #ifndef AL_SOFT_MSADPCM #define AL_SOFT_MSADPCM 1 #define AL_FORMAT_MONO_MSADPCM_SOFT 0x1302 #define AL_FORMAT_STEREO_MSADPCM_SOFT 0x1303 #endif #ifndef AL_SOFT_source_length #define AL_SOFT_source_length 1 /*#define AL_BYTE_LENGTH_SOFT 0x2009*/ /*#define AL_SAMPLE_LENGTH_SOFT 0x200A*/ /*#define AL_SEC_LENGTH_SOFT 0x200B*/ #endif #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); #ifdef AL_ALEXT_PROTOTYPES 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 /* 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 #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); #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); #endif #endif #ifndef AL_SOFT_gain_clamp_ex #define AL_SOFT_gain_clamp_ex 1 #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); #ifdef AL_ALEXT_PROTOTYPES 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 #endif #ifndef ALC_SOFT_output_limiter #define ALC_SOFT_output_limiter #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 } #endif #endif openmw-openmw-0.49.0/apps/openmw/mwsound/constants.hpp000066400000000000000000000010541503074453300231110ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwsound/efx-presets.h000066400000000000000000002113051503074453300230040ustar00rootroot00000000000000/* Reverb presets for EFX */ #ifndef EFX_PRESETS_H #define EFX_PRESETS_H #ifndef EFXEAXREVERBPROPERTIES_DEFINED #define EFXEAXREVERBPROPERTIES_DEFINED typedef struct { float flDensity; float flDiffusion; float flGain; float flGainHF; float flGainLF; float flDecayTime; float flDecayHFRatio; float flDecayLFRatio; float flReflectionsGain; float flReflectionsDelay; float flReflectionsPan[3]; float flLateReverbGain; float flLateReverbDelay; float flLateReverbPan[3]; float flEchoTime; float flEchoDepth; float flModulationTime; float flModulationDepth; float flAirAbsorptionGainHF; float flHFReference; float flLFReference; float flRoomRolloffFactor; 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } /* 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 \ } #endif /* EFX_PRESETS_H */ openmw-openmw-0.49.0/apps/openmw/mwsound/efx.h000066400000000000000000000660161503074453300213300ustar00rootroot00000000000000#ifndef AL_EFX_H #define AL_EFX_H #include "al.h" #ifdef __cplusplus 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 /* Listener properties. */ #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 /* 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 /* 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 /* 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 /* 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 /* 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 /* 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 /* Frequency shifter effect parameters */ #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 /* Pitchshifter effect parameters */ #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 /* 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 /* Compressor effect parameters */ #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 /* Effect type */ #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 /* Auxiliary Effect Slot properties. */ #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 /* Filter properties. */ /* Lowpass filter parameters */ #define AL_LOWPASS_GAIN 0x0001 #define AL_LOWPASS_GAINHF 0x0002 /* Highpass filter parameters */ #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 /* Filter type */ #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*); #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); #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_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_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_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_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_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_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_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_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_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 /* 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_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_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_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_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_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_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_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_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_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_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_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 /* Chorus effect */ #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_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_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_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_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_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) /* 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_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_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) /* Flanger effect */ #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_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_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_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_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_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_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_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) /* 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_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_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_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_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_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) /* 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_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_CENTER (200.0f) #define AL_EQUALIZER_MAX_MID1_CENTER (3000.0f) #define AL_EQUALIZER_DEFAULT_MID1_CENTER (500.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_GAIN (0.126f) #define AL_EQUALIZER_MAX_MID2_GAIN (7.943f) #define AL_EQUALIZER_DEFAULT_MID2_GAIN (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_WIDTH (0.01f) #define AL_EQUALIZER_MAX_MID2_WIDTH (1.0f) #define AL_EQUALIZER_DEFAULT_MID2_WIDTH (1.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_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_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_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) #ifdef __cplusplus } /* extern "C" */ #endif #endif /* AL_EFX_H */ openmw-openmw-0.49.0/apps/openmw/mwsound/ffmpeg_decoder.cpp000066400000000000000000000425111503074453300240240ustar00rootroot00000000000000#include "ffmpeg_decoder.hpp" #include #include #include #include #include #include #include #include #if OPENMW_FFMPEG_5_OR_GREATER #include #endif namespace MWSound { void AVIOContextDeleter::operator()(AVIOContext* ptr) const { 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 } void AVFormatContextDeleter::operator()(AVFormatContext* ptr) const { avformat_close_input(&ptr); } void AVCodecContextDeleter::operator()(AVCodecContext* ptr) const { avcodec_free_context(&ptr); } void AVFrameDeleter::operator()(AVFrame* ptr) const { av_frame_free(&ptr); } int FFmpeg_Decoder::readPacket(void* user_data, uint8_t* buf, int buf_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 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; } int64_t FFmpeg_Decoder::seek(void* user_data, int64_t offset, int whence) { std::istream& stream = *static_cast(user_data)->mDataStream; whence &= ~AVSEEK_FORCE; stream.clear(); if (whence == AVSEEK_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; } 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; return stream.tellg(); } /* 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; std::ptrdiff_t stream_idx = mStream - mFormatCtx->streams; while (av_read_frame(mFormatCtx.get(), &mPacket) >= 0) { /* 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); } return false; } bool FFmpeg_Decoder::getAVAudioData() { bool got_frame = false; if (mCodecCtx->codec_type != AVMEDIA_TYPE_AUDIO) return false; do { /* Decode some data, and check for errors */ int ret = avcodec_receive_frame(mCodecCtx.get(), mFrame.get()); if (ret == AVERROR(EAGAIN)) { if (mPacket.size == 0 && !getNextPacket()) return false; 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 (mSwr) { 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; } 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]; } while (!got_frame); mNextPts += (double)mFrame->nb_samples / mCodecCtx->sample_rate; return true; } size_t FFmpeg_Decoder::readAVAudioData(void* data, size_t length) { size_t dec = 0; while (dec < length) { /* 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); /* 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; } void FFmpeg_Decoder::open(VFS::Path::NormalizedView 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"); AVFormatContext* formatCtx = avformat_alloc_context(); if (formatCtx == nullptr) throw std::runtime_error("Failed to allocate context"); 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"); AVFormatContextPtr formatCtxPtr(std::exchange(formatCtx, nullptr)); 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 (formatCtxPtr->streams[j]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { stream = &formatCtxPtr->streams[j]; break; } } 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* 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, (*stream)->time_base); #endif AVCodecContextPtr codecCtxPtr(std::exchange(codecCtx, nullptr)); if (avcodec_open2(codecCtxPtr.get(), codec, nullptr) < 0) throw std::runtime_error(std::string("Failed to open audio codec ") + codec->long_name); AVFramePtr frame(av_frame_alloc()); if (frame == nullptr) throw std::runtime_error("Failed to allocate frame"); if (codecCtxPtr->sample_fmt == AV_SAMPLE_FMT_U8P) mOutputSampleFormat = AV_SAMPLE_FMT_U8; // 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; #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); 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; } void FFmpeg_Decoder::close() { mStream = nullptr; mCodecCtx.reset(); av_packet_unref(&mPacket); av_freep(&mDataBuf); mFrame.reset(); swr_free(&mSwr); mFormatCtx.reset(); mIoCtx.reset(); mDataStream.reset(); } 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 } 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 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_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; } *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); if (mOutputSampleFormat != mCodecCtx->sample_fmt || mOutputChannelLayout.u.mask != ch_layout.u.mask) #else 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 { #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)); } } size_t FFmpeg_Decoder::read(char* buffer, size_t bytes) { if (!mStream) { Log(Debug::Error) << "No audio stream"; return 0; } return readAVAudioData(buffer, bytes); } void FFmpeg_Decoder::readAll(std::vector& output) { if (!mStream) { Log(Debug::Error) << "No audio stream"; return; } 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); } } size_t FFmpeg_Decoder::getSampleOffset() { #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; } 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) { 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) { // This is not needed anymore above FFMpeg version 4.0 #if LIBAVCODEC_VERSION_INT < 3805796 av_register_all(); #endif av_log_set_level(AV_LOG_ERROR); done_init = true; } } FFmpeg_Decoder::~FFmpeg_Decoder() { close(); } } openmw-openmw-0.49.0/apps/openmw/mwsound/ffmpeg_decoder.hpp000066400000000000000000000064341503074453300240350ustar00rootroot00000000000000#ifndef GAME_SOUND_FFMPEG_DECODER_H #define GAME_SOUND_FFMPEG_DECODER_H #include #include #include #if defined(_MSC_VER) #pragma warning(push) #pragma warning(disable : 4244) #endif extern "C" { #include #include #include // From version 54.56 binkaudio encoding format changed from S16 to FLTP. See: // https://gitorious.org/ffmpeg/ffmpeg/commit/7bfd1766d1c18f07b0a2dd042418a874d49ea60d // https://ffmpeg.zeranoe.com/forum/viewtopic.php?f=15&t=872 #include } #if defined(_MSC_VER) #pragma warning(pop) #endif #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 { AVIOContextPtr mIoCtx; AVFormatContextPtr mFormatCtx; AVCodecContextPtr mCodecCtx; AVStream** mStream; AVPacket mPacket; AVFramePtr mFrame; std::size_t mFrameSize; std::size_t mFramePos; double mNextPts; SwrContext* mSwr; enum AVSampleFormat mOutputSampleFormat; #if OPENMW_FFMPEG_5_OR_GREATER AVChannelLayout mOutputChannelLayout; #else int64_t mOutputChannelLayout; #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); #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); void open(VFS::Path::NormalizedView fname) override; 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 readAll(std::vector& output) override; size_t getSampleOffset() override; FFmpeg_Decoder& operator=(const FFmpeg_Decoder& rhs); FFmpeg_Decoder(const FFmpeg_Decoder& rhs); public: explicit FFmpeg_Decoder(const VFS::Manager* vfs); virtual ~FFmpeg_Decoder(); friend class SoundManager; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/loudness.cpp000066400000000000000000000044261503074453300227320ustar00rootroot00000000000000#include "loudness.hpp" #include #include #include #include namespace MWSound { 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); std::size_t segment = 0; std::size_t sample = 0; while (segment < numSamples / samplesPerSegment) { float sum = 0; int samplesAdded = 0; while (sample < numSamples && sample < (segment + 1) * 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) { 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; } 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); } float Sound_Loudness::getLoudnessAtTime(float sec) const { if (mSamplesPerSec <= 0.0f || mSamples.empty() || sec < 0.0f) return 0.0f; size_t index = std::min(static_cast(sec * mSamplesPerSec), mSamples.size() - 1); return mSamples[index]; } } openmw-openmw-0.49.0/apps/openmw/mwsound/loudness.hpp000066400000000000000000000037701503074453300227400ustar00rootroot00000000000000#ifndef GAME_SOUND_LOUDNESS_H #define GAME_SOUND_LOUDNESS_H #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; }; } #endif /* GAME_SOUND_LOUDNESS_H */ openmw-openmw-0.49.0/apps/openmw/mwsound/movieaudiofactory.cpp000066400000000000000000000136351503074453300246310ustar00rootroot00000000000000#include "movieaudiofactory.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "sound_decoder.hpp" namespace MWSound { class MovieAudioDecoder; class MWSoundDecoderBridge final : public Sound_Decoder { public: MWSoundDecoderBridge(MWSound::MovieAudioDecoder* decoder) : Sound_Decoder(nullptr) , mDecoder(decoder) { } private: MWSound::MovieAudioDecoder* mDecoder; 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; size_t getSampleOffset() override; }; class MovieAudioDecoder : public Video::MovieAudioDecoder { public: MovieAudioDecoder(Video::VideoState* videoState) : Video::MovieAudioDecoder(videoState) , mAudioTrack(nullptr) , mDecoderBridge(std::make_shared(this)) { } size_t getSampleOffset() { #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() { return std::string(); } private: // MovieAudioDecoder overrides double getAudioClock() override { 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; // 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_STEREO; else if (channelLayout != AV_CH_LAYOUT_MONO && channelLayout != AV_CH_LAYOUT_STEREO) channelLayout = AV_CH_LAYOUT_STEREO; } public: ~MovieAudioDecoder() { if (mAudioTrack) MWBase::Environment::get().getSoundManager()->stopTrack(mAudioTrack); mAudioTrack = nullptr; mDecoderBridge.reset(); } MWBase::SoundStream* mAudioTrack; std::shared_ptr mDecoderBridge; }; std::string MWSoundDecoderBridge::getName() { return mDecoder->getStreamName(); } void MWSoundDecoderBridge::getInfo(int* samplerate, ChannelConfig* chans, SampleType* type) { *samplerate = mDecoder->getOutputSampleRate(); uint64_t outputChannelLayout = mDecoder->getOutputChannelLayout(); if (outputChannelLayout == AV_CH_LAYOUT_MONO) *chans = ChannelConfig_Mono; else if (outputChannelLayout == AV_CH_LAYOUT_5POINT1) *chans = ChannelConfig_5point1; else if (outputChannelLayout == AV_CH_LAYOUT_7POINT1) *chans = ChannelConfig_7point1; else if (outputChannelLayout == AV_CH_LAYOUT_STEREO) *chans = ChannelConfig_Stereo; else if (outputChannelLayout == AV_CH_LAYOUT_QUAD) *chans = ChannelConfig_Quad; else throw std::runtime_error("Unsupported channel layout: " + std::to_string(outputChannelLayout)); AVSampleFormat outputSampleFormat = mDecoder->getOutputSampleFormat(); if (outputSampleFormat == AV_SAMPLE_FMT_U8) *type = SampleType_UInt8; else if (outputSampleFormat == AV_SAMPLE_FMT_FLT) *type = SampleType_Float32; else if (outputSampleFormat == AV_SAMPLE_FMT_S16) *type = SampleType_Int16; else { char str[1024]; av_get_sample_fmt_string(str, sizeof(str), outputSampleFormat); throw std::runtime_error(std::string("Unsupported sample format: ") + str); } } size_t MWSoundDecoderBridge::read(char* buffer, size_t bytes) { return mDecoder->read(buffer, bytes); } size_t MWSoundDecoderBridge::getSampleOffset() { return mDecoder->getSampleOffset(); } std::unique_ptr MovieAudioFactory::createDecoder(Video::VideoState* 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); if (!sound) { decoder.reset(); return decoder; } decoder->mAudioTrack = sound; return decoder; } } openmw-openmw-0.49.0/apps/openmw/mwsound/movieaudiofactory.hpp000066400000000000000000000005461503074453300246330ustar00rootroot00000000000000#ifndef OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H #define OPENMW_MWSOUND_MOVIEAUDIOFACTORY_H #include namespace MWSound { class MovieAudioFactory : public Video::MovieAudioFactory { std::unique_ptr createDecoder(Video::VideoState* videoState) override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/openal_output.cpp000066400000000000000000001530411503074453300237720ustar00rootroot00000000000000#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.hpp" #include "sound_decoder.hpp" #include "soundmanagerimp.hpp" #include "efx-presets.h" #ifndef ALC_ALL_DEVICES_SPECIFIER #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 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; } #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); } // 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) { 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(); } 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 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; } } } 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 (fmt.chans == chans && fmt.type == type) { ALenum format = alGetEnumValue(fmt.name); 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; } // // A streaming OpenAL sound. // class OpenAL_SoundStream { static const ALfloat sBufferLength; private: ALuint mSource; std::array mBuffers; ALint mCurrentBufIdx; ALenum mFormat; ALsizei mSampleRate; ALuint mBufferSize; ALuint mFrameSize; ALint mSilence; DecoderPtr mDecoder; std::unique_ptr mLoudnessAnalyzer; std::atomic mIsFinished; OpenAL_SoundStream(const OpenAL_SoundStream& rhs); OpenAL_SoundStream& operator=(const OpenAL_SoundStream& rhs); friend class OpenAL_Output; public: OpenAL_SoundStream(ALuint src, DecoderPtr decoder); ~OpenAL_SoundStream(); bool init(bool getLoudnessData = false); bool isPlaying(); double getStreamDelay() const; double getStreamOffset() const; float getCurrentLoudness() const; 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; StreamThread() : mQuitNow(false) , mThread([this] { run(); }) { } ~StreamThread() { mQuitNow = true; mMutex.lock(); mMutex.unlock(); mCondVar.notify_all(); mThread.join(); } // 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; } mCondVar.wait_for(lock, std::chrono::milliseconds(50)); } } void add(OpenAL_SoundStream* stream) { std::lock_guard lock(mMutex); if (std::find(mStreams.begin(), mStreams.end(), stream) == mStreams.end()) { mStreams.push_back(stream); mCondVar.notify_all(); } } 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 removeAll() { std::lock_guard lock(mMutex); mStreams.clear(); } StreamThread(const StreamThread& rhs) = delete; StreamThread& operator=(const StreamThread& rhs) = delete; }; class OpenAL_Output::DefaultDeviceThread { public: std::basic_string mCurrentName; private: OpenAL_Output& mOutput; std::atomic mQuitNow; std::mutex mMutex; std::condition_variable mCondVar; std::thread mThread; DefaultDeviceThread(const DefaultDeviceThread&) = delete; DefaultDeviceThread& operator=(const DefaultDeviceThread&) = delete; void run() { Misc::setCurrentThreadIdlePriority(); std::unique_lock lock(mMutex); while (!mQuitNow) { { const std::lock_guard openLock(mOutput.mReopenMutex); std::basic_string_view defaultName = getDeviceName(nullptr); if (mCurrentName != defaultName) { mCurrentName = defaultName; Log(Debug::Info) << "Default audio device changed to \"" << mCurrentName << "\""; ALCboolean reopened = alcReopenDeviceSOFT( mOutput.mDevice, mCurrentName.data(), mOutput.mContextAttributes.data()); if (reopened == AL_FALSE) Log(Debug::Warning) << "Failed to switch to new audio device"; } } mCondVar.wait_for(lock, std::chrono::seconds(2)); } } public: DefaultDeviceThread(OpenAL_Output& output, std::basic_string_view name = {}) : mCurrentName(name) , mOutput(output) , mQuitNow(false) , mThread([this] { run(); }) { } ~DefaultDeviceThread() { mQuitNow = true; mMutex.lock(); mMutex.unlock(); mCondVar.notify_all(); mThread.join(); } }; 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); } OpenAL_SoundStream::~OpenAL_SoundStream() { if (mBuffers[0] && alIsBuffer(mBuffers[0])) alDeleteBuffers(mBuffers.size(), mBuffers.data()); alGetError(); mDecoder->close(); } bool OpenAL_SoundStream::init(bool getLoudnessData) { alGenBuffers(mBuffers.size(), mBuffers.data()); ALenum err = getALError(); if (err != AL_NO_ERROR) return false; ChannelConfig chans; SampleType type; 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; } switch (type) { case SampleType_UInt8: mSilence = 0x80; break; case SampleType_Int16: mSilence = 0x00; break; case SampleType_Float32: mSilence = 0x00; break; } mFrameSize = framesToBytes(1, chans, type); mBufferSize = static_cast(sBufferLength * mSampleRate); mBufferSize *= mFrameSize; if (getLoudnessData) mLoudnessAnalyzer = std::make_unique(sLoudnessFPS, mSampleRate, chans, type); mIsFinished = false; return true; } bool OpenAL_SoundStream::isPlaying() { ALint state; alGetSourcei(mSource, AL_SOURCE_STATE, &state); getALError(); if (state == AL_PLAYING || state == AL_PAUSED) return true; return !mIsFinished; } double OpenAL_SoundStream::getStreamDelay() const { ALint state = AL_STOPPED; double d = 0.0; ALint offset; 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; } getALError(); return d; } double OpenAL_SoundStream::getStreamOffset() const { ALint state = AL_STOPPED; ALint offset; double 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; 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; } float OpenAL_SoundStream::getCurrentLoudness() const { if (!mLoudnessAnalyzer.get()) return 0.f; float time = getStreamOffset(); return mLoudnessAnalyzer->getLoudnessAtTime(time); } bool OpenAL_SoundStream::process() { try { if (refillQueue() > 0) { 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); } } } catch (const std::exception& e) { Log(Debug::Error) << "Error updating stream \"" << mDecoder->getName() << "\": " << e.what(); mIsFinished = true; } return !mIsFinished; } ALint OpenAL_SoundStream::refillQueue() { ALint processed; alGetSourcei(mSource, AL_BUFFERS_PROCESSED, &processed); while (processed > 0) { ALuint buf; alSourceUnqueueBuffers(mSource, 1, &buf); --processed; } ALint queued; alGetSourcei(mSource, AL_BUFFERS_QUEUED, &queued); if (!mIsFinished && (ALuint)queued < mBuffers.size()) { std::vector data(mBufferSize); for (; !mIsFinished && (ALuint)queued < mBuffers.size(); ++queued) { size_t got = mDecoder->read(data.data(), data.size()); if (got < data.size()) { mIsFinished = true; std::fill(data.begin() + got, data.end(), mSilence); } if (got > 0) { if (mLoudnessAnalyzer.get()) mLoudnessAnalyzer->analyzeLoudness(data); ALuint bufid = mBuffers[mCurrentBufIdx]; alBufferData(bufid, mFormat, data.data(), data.size(), mSampleRate); alSourceQueueBuffers(mSource, 1, &bufid); mCurrentBufIdx = (mCurrentBufIdx + 1) % mBuffers.size(); } } } 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; } void OpenAL_Output::eventCallback( ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam) { if (eventType == AL_EVENT_TYPE_DISCONNECTED_SOFT) static_cast(userParam)->onDisconnect(); } void OpenAL_Output::onDisconnect() { 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()) { reopened = alcReopenDeviceSOFT(mDevice, nullptr, mContextAttributes.data()); if (reopened == AL_TRUE && !mDefaultDeviceThread) mDefaultDeviceThread = std::make_unique(*this); } if (reopened == AL_FALSE) Log(Debug::Error) << "Failed to reopen audio device"; else { Log(Debug::Info) << "Reopened audio device"; if (mDefaultDeviceThread) mDefaultDeviceThread->mCurrentName = getDeviceName(mDevice); } } 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..."; mDeviceName = devname; mDevice = alcOpenDevice(devname.c_str()); if (!mDevice && !devname.empty()) { Log(Debug::Warning) << "Failed to open \"" << devname << "\", trying default"; mDevice = alcOpenDevice(nullptr); mDeviceName.clear(); } if (!mDevice) { Log(Debug::Error) << "Failed to open default audio device"; return 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); ALC.EXT_EFX = alcIsExtensionPresent(mDevice, "ALC_EXT_EFX"); ALC.SOFT_HRTF = alcIsExtensionPresent(mDevice, "ALC_SOFT_HRTF"); 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; } } 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); 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; } Log(Debug::Info) << " Vendor: " << alGetString(AL_VENDOR) << "\n" << " Renderer: " << alGetString(AL_RENDERER) << "\n" << " Version: " << alGetString(AL_VERSION) << "\n" << " Extensions: " << alGetString(AL_EXTENSIONS); 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"; } if (!ALC.SOFT_HRTF) Log(Debug::Warning) << "HRTF status unavailable"; else { 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; } } 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()) { 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; } 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(); } alGenAuxiliaryEffectSlots(1, &mEffectSlot); alGetError(); 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(); mInitialized = true; return true; } void OpenAL_Output::deinit() { 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; } std::vector OpenAL_Output::enumerateHrtf() { 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; } std::pair OpenAL_Output::loadSound(VFS::Path::NormalizedView fname) { getALError(); std::vector data; ALenum format = AL_NONE; int srate = 0; 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(); } if (data.empty()) { // If we failed to get any usable audio, substitute with silence. format = AL_FORMAT_MONO8; srate = 8000; data.assign(8000, -128); } 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); } size_t OpenAL_Output::unloadSound(Sound_Handle data) { 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) { 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); } } ALint size = 0; alGetBufferi(buffer, AL_SIZE, &size); alDeleteBuffers(1, &buffer); getALError(); return 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) { 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); } 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); } 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 (useenv && mListenerEnv == Env_Underwater && !mWaterFilter) { gain *= 0.9f; pitch *= 0.7f; } 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); } bool OpenAL_Output::playSound(Sound* sound, Sound_Handle data, float offset) { ALuint source; if (mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } source = mFreeSources.front(); 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; } bool OpenAL_Output::playSound3D(Sound* sound, Sound_Handle data, float offset) { ALuint source; 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; } 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; } 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)); } bool OpenAL_Output::isSoundPlaying(Sound* sound) { if (!sound->mHandle) return false; ALuint source = GET_PTRID(sound->mHandle); ALint state = AL_STOPPED; alGetSourcei(source, AL_SOURCE_STATE, &state); getALError(); return state == AL_PLAYING || state == AL_PAUSED; } void OpenAL_Output::updateSound(Sound* sound) { if (!sound->mHandle) return; ALuint source = GET_PTRID(sound->mHandle); updateCommon(source, sound->getPosition(), sound->getMaxDistance(), sound->getRealVolume(), getTimeScaledPitch(sound), sound->getUseEnv()); getALError(); } bool OpenAL_Output::streamSound(DecoderPtr decoder, Stream* sound, bool getLoudnessData) { if (mFreeSources.empty()) { Log(Debug::Warning) << "No free sources!"; return false; } ALuint source = mFreeSources.front(); if (sound->getIsLooping()) Log(Debug::Warning) << "Warning: cannot loop stream \"" << decoder->getName() << "\""; initCommon2D( source, sound->getPosition(), sound->getRealVolume(), getTimeScaledPitch(sound), false, sound->getUseEnv()); if (getALError() != AL_NO_ERROR) return false; 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()) { Log(Debug::Warning) << "No free sources!"; return false; } 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(), getTimeScaledPitch(sound), false, sound->getUseEnv()); if (getALError() != AL_NO_ERROR) return false; 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; } 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(), getTimeScaledPitch(sound), sound->getUseEnv()); getALError(); } void OpenAL_Output::startUpdate() { alcSuspendContext(alcGetCurrentContext()); } void OpenAL_Output::finishUpdate() { alcProcessContext(alcGetCurrentContext()); } void OpenAL_Output::updateListener( const osg::Vec3f& pos, const osg::Vec3f& atdir, const osg::Vec3f& updir, Environment env) { if (mContext) { ALfloat orient[6] = { atdir.x(), atdir.y(), atdir.z(), updir.x(), updir.y(), updir.z() }; alListenerfv(AL_POSITION, pos.ptr()); alListenerfv(AL_ORIENTATION, orient); if (env != mListenerEnv) { alSpeedOfSound(((env == Env_Underwater) ? Constants::SoundSpeedUnderwater : Constants::SoundSpeedInAir) * Constants::UnitsPerMeter); // Update active sources with the environment's direct filter if (mWaterFilter) { 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); } 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)); } 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(); } } void OpenAL_Output::pauseActiveDevice() { if (mDevice == nullptr) return; if (alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { LPALCDEVICEPAUSESOFT alcDevicePauseSOFT = nullptr; getALCFunc(alcDevicePauseSOFT, mDevice, "alcDevicePauseSOFT"); alcDevicePauseSOFT(mDevice); getALCError(mDevice); } alListenerf(AL_GAIN, 0.0f); } void OpenAL_Output::resumeActiveDevice() { if (mDevice == nullptr) return; if (alcIsExtensionPresent(mDevice, "ALC_SOFT_PAUSE_DEVICE")) { LPALCDEVICERESUMESOFT alcDeviceResumeSOFT = nullptr; getALCFunc(alcDeviceResumeSOFT, mDevice, "alcDeviceResumeSOFT"); alcDeviceResumeSOFT(mDevice); getALCError(mDevice); } alListenerf(AL_GAIN, 1.0f); } void OpenAL_Output::resumeSounds(int types) { 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()) { 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(std::make_unique()) { } 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(); } } openmw-openmw-0.49.0/apps/openmw/mwsound/openal_output.hpp000066400000000000000000000075331503074453300240030ustar00rootroot00000000000000#ifndef GAME_SOUND_OPENAL_OUTPUT_H #define GAME_SOUND_OPENAL_OUTPUT_H #include #include #include #include #include #include #include "al.h" #include "alc.h" #include "alext.h" #include "sound_output.hpp" namespace MWSound { class SoundManager; class SoundBase; class Sound; class Stream; class OpenAL_Output : public Sound_Output { ALCdevice* mDevice; ALCcontext* mContext; struct { bool EXT_EFX : 1; bool SOFT_HRTF : 1; } ALC = { false, false }; struct { bool SOFT_source_spatialize : 1; } AL = { false }; typedef std::deque IDDq; IDDq mFreeSources; typedef std::vector SoundVec; SoundVec mActiveSounds; typedef std::vector StreamVec; StreamVec mActiveStreams; osg::Vec3f mListenerPos; Environment mListenerEnv; ALuint mWaterFilter; ALuint mWaterEffect; ALuint mDefaultEffect; ALuint mEffectSlot; struct StreamThread; std::unique_ptr mStreamThread; 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); static void eventCallback( ALenum eventType, ALuint object, ALuint param, ALsizei length, const ALchar* message, void* userParam); void onDisconnect(); public: std::vector enumerate() override; bool init(const std::string& devname, const std::string& hrtfname, HrtfMode hrtfmode) override; void deinit() override; std::vector enumerateHrtf() 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; void startUpdate() override; void finishUpdate() 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; void pauseActiveDevice() override; void resumeActiveDevice() override; OpenAL_Output(SoundManager& mgr); virtual ~OpenAL_Output(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/regionsoundselector.cpp000066400000000000000000000024621503074453300251710ustar00rootroot00000000000000#include "regionsoundselector.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" namespace MWSound { RegionSoundSelector::RegionSoundSelector() : mMinTimeBetweenSounds(Fallback::Map::getFloat("Weather_Minimum_Time_Between_Environmental_Sounds")) , mMaxTimeBetweenSounds(Fallback::Map::getFloat("Weather_Maximum_Time_Between_Environmental_Sounds")) { } ESM::RefId RegionSoundSelector::getNextRandom(float duration, const ESM::RefId& regionName) { mTimePassed += duration; if (mTimePassed < mTimeToNextEnvSound) return {}; const float a = Misc::Rng::rollClosedProbability(); mTimeToNextEnvSound = mMinTimeBetweenSounds + (mMaxTimeBetweenSounds - mMinTimeBetweenSounds) * a; mTimePassed = 0; const ESM::Region* const region = MWBase::Environment::get().getESMStore()->get().search(regionName); if (region == nullptr) return {}; for (const ESM::Region::SoundRef& sound : region->mSoundList) { if (Misc::Rng::roll0to99() < sound.mChance) return sound.mSound; } return {}; } } openmw-openmw-0.49.0/apps/openmw/mwsound/regionsoundselector.hpp000066400000000000000000000007401503074453300251730ustar00rootroot00000000000000#ifndef GAME_SOUND_REGIONSOUNDSELECTOR_H #define GAME_SOUND_REGIONSOUNDSELECTOR_H #include namespace MWSound { class RegionSoundSelector { public: ESM::RefId getNextRandom(float duration, const ESM::RefId& regionName); RegionSoundSelector(); private: float mTimeToNextEnvSound = 0.0f; float mTimePassed = 0.0; float mMinTimeBetweenSounds; float mMaxTimeBetweenSounds; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/sound.hpp000066400000000000000000000161631503074453300222340ustar00rootroot00000000000000#ifndef GAME_SOUND_SOUND_H #define GAME_SOUND_SOUND_H #include #include "sound_output.hpp" namespace MWSound { // Extra play flags, not intended for caller use 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); } struct SoundParams { osg::Vec3f mPos; float mVolume = 1.0f; float mBaseVolume = 1.0f; float mPitch = 1.0f; float mMinDistance = 1.0f; float mMaxDistance = 1000.0f; int mFlags = 0; float mFadeVolume = 1.0f; float mFadeTarget = 0.0f; float mFadeStep = 0.0f; }; class SoundBase { SoundBase& operator=(const SoundBase&) = delete; SoundBase(const SoundBase&) = delete; SoundBase(SoundBase&&) = delete; SoundParams mParams; protected: Sound_Instance mHandle = nullptr; friend class OpenAL_Output; public: 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) { setFade(duration, 0.0, Play_StopAtFadeEnd); } /// Fade to the given linear gain within the specified amount of time. /// Note that the fade gain is independent of the sound volume. /// /// \param duration specifies the duration of the fade. For *linear* /// fades (default) this will be exactly the time at which the desired /// volume is reached. Let v0 be the initial volume, v1 be the target /// volume, and t0 be the initial time. Then the volume over time is /// given as /// /// v(t) = v0 + (v1 - v0) * (t - t0) / duration if t <= t0 + duration /// v(t) = v1 if t > t0 + duration /// /// For *exponential* fades this determines the time-constant of the /// exponential process describing the fade. In particular, we guarantee /// that we reach v0 + 0.99 * (v1 - v0) within the given duration. /// /// v(t) = v1 + (v0 - v1) * exp(-4.6 * (t0 - t) / duration) /// /// where -4.6 is approximately log(1%) (i.e., -40 dB). /// /// This interpolation mode is meant for environmental sound effects to /// achieve less jarring transitions. /// /// \param targetVolume is the linear gain that should be reached at /// the end of the fade. /// /// \param flags may be a combination of Play_FadeExponential and /// Play_StopAtFadeEnd. If Play_StopAtFadeEnd is set, stops the sound /// once the fade duration has passed or the target volume has been /// reached. If Play_FadeExponential is set, enables the exponential /// fade mode (see above). void setFade(float duration, float targetVolume, int flags = 0) { // Approximation of log(1%) (i.e., -40 dB). constexpr float minus40Decibel = -4.6f; // Do nothing if already at the target, unless we need to trigger a stop event if ((mParams.mFadeVolume == targetVolume) && !(flags & Play_StopAtFadeEnd)) return; mParams.mFadeTarget = targetVolume; mParams.mFlags = (mParams.mFlags & ~Play_FadeFlagsMask) | (flags & Play_FadeFlagsMask) | Play_InFade; if (duration > 0.0f) { if (mParams.mFlags & Play_FadeExponential) mParams.mFadeStep = -minus40Decibel / duration; else mParams.mFadeStep = (mParams.mFadeTarget - mParams.mFadeVolume) / duration; } else { mParams.mFadeVolume = mParams.mFadeTarget; mParams.mFadeStep = 0.0f; } } /// Updates the internal fading logic. /// /// \param dt is the time in seconds since the last call to update. /// /// \return true if the sound is still active, false if the sound has /// reached a fading destination that was marked with Play_StopAtFadeEnd. bool updateFade(float dt) { // 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); } 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) { mParams = params; mHandle = nullptr; } SoundBase() = default; }; class Sound : public SoundBase { Sound& operator=(const Sound&) = delete; Sound(const Sound&) = delete; Sound(Sound&&) = delete; public: Sound() = default; }; class Stream : public SoundBase { Stream& operator=(const Stream&) = delete; Stream(const Stream&) = delete; Stream(Stream&&) = delete; public: Stream() = default; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/sound_buffer.cpp000066400000000000000000000145421503074453300235570ustar00rootroot00000000000000#include "sound_buffer.hpp" #include "../mwbase/environment.hpp" #include "../mwworld/esmstore.hpp" #include #include #include #include #include #include #include namespace MWSound { namespace { struct AudioParams { float mAudioDefaultMinDistance; float mAudioDefaultMaxDistance; float mAudioMinDistanceMult; float mAudioMaxDistanceMult; }; AudioParams makeAudioParams(const MWWorld::Store& settings) { AudioParams params; params.mAudioDefaultMinDistance = settings.find("fAudioDefaultMinDistance")->mValue.getFloat(); params.mAudioDefaultMaxDistance = settings.find("fAudioDefaultMaxDistance")->mValue.getFloat(); params.mAudioMinDistanceMult = settings.find("fAudioMinDistanceMult")->mValue.getFloat(); params.mAudioMaxDistanceMult = settings.find("fAudioMaxDistanceMult")->mValue.getFloat(); return params; } } SoundBufferPool::SoundBufferPool(Sound_Output& output) : mOutput(&output) , mBufferCacheMax(Settings::sound().mBufferCacheMax * 1024 * 1024) , mBufferCacheMin( std::min(static_cast(Settings::sound().mBufferCacheMin) * 1024 * 1024, mBufferCacheMax)) { } SoundBufferPool::~SoundBufferPool() { clear(); } Sound_Buffer* SoundBufferPool::lookup(const ESM::RefId& soundId) const { const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) { Sound_Buffer* sfx = it->second; if (sfx->getHandle() != nullptr) return sfx; } return nullptr; } 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().getESMStore()->get()) insertSound(sound.mId, sound); } Sound_Buffer* sfx; const auto it = mBufferNameMap.find(soundId); if (it != mBufferNameMap.end()) sfx = it->second; else { const ESM::Sound* sound = MWBase::Environment::get().getESMStore()->get().search(soundId); if (sound == nullptr) return {}; sfx = insertSound(soundId, *sound); } return loadSfx(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 loadSfx(sfx); } void SoundBufferPool::clear() { for (auto& sfx : mSoundBuffers) { if (sfx.mHandle) mOutput->unloadSound(sfx.mHandle); sfx.mHandle = nullptr; } mBufferFileNameMap.clear(); mBufferNameMap.clear(); mUnusedBuffers.clear(); } 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().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; float max = sound.mData.mMaxRange; if (min == 0 && max == 0) { min = audioParams.mAudioDefaultMinDistance; max = audioParams.mAudioDefaultMaxDistance; } min *= audioParams.mAudioMinDistanceMult; max *= audioParams.mAudioMaxDistanceMult; min = std::max(min, 1.0f); max = std::max(min, max); Sound_Buffer& sfx = mSoundBuffers.emplace_back( Misc::ResourceHelpers::correctSoundPath(VFS::Path::Normalized(sound.mSound)), volume, min, max); mBufferNameMap.emplace(soundId, &sfx); return &sfx; } void SoundBufferPool::unloadUnused() { while (!mUnusedBuffers.empty() && mBufferCacheSize > mBufferCacheMin) { Sound_Buffer* const unused = mUnusedBuffers.back(); mBufferCacheSize -= mOutput->unloadSound(unused->getHandle()); unused->mHandle = nullptr; mUnusedBuffers.pop_back(); } } } openmw-openmw-0.49.0/apps/openmw/mwsound/sound_buffer.hpp000066400000000000000000000064661503074453300235720ustar00rootroot00000000000000#ifndef GAME_SOUND_SOUND_BUFFER_H #define GAME_SOUND_SOUND_BUFFER_H #include #include #include #include #include "sound_output.hpp" #include namespace ESM { struct Sound; } namespace VFS { class Manager; } namespace MWSound { class SoundBufferPool; 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) { } const VFS::Path::Normalized& getResourceName() const noexcept { return mResourceName; } Sound_Handle getHandle() const noexcept { return mHandle; } float getVolume() const noexcept { return mVolume; } float getMinDist() const noexcept { return mMinDist; } float getMaxDist() const noexcept { return mMaxDist; } private: VFS::Path::Normalized mResourceName; float mVolume; float mMinDist; float mMaxDist; Sound_Handle mHandle = nullptr; std::size_t mUses = 0; friend class SoundBufferPool; }; class SoundBufferPool { public: SoundBufferPool(Sound_Output& output); SoundBufferPool(const SoundBufferPool&) = delete; ~SoundBufferPool(); /// Lookup a soundId for its sound data (resource name, local volume, /// minRange, and maxRange) Sound_Buffer* lookup(const ESM::RefId& soundId) const; /// Lookup a sound by file name for its sound data (resource name, local volume, /// minRange, and maxRange) Sound_Buffer* lookup(std::string_view fileName) const; /// 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); // 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) { 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(); private: Sound_Buffer* loadSfx(Sound_Buffer* sfx); 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 ESM::RefId& soundId, const ESM::Sound& sound); inline Sound_Buffer* insertSound(std::string_view fileName); inline void unloadUnused(); }; } #endif /* GAME_SOUND_SOUND_BUFFER_H */ openmw-openmw-0.49.0/apps/openmw/mwsound/sound_decoder.hpp000066400000000000000000000030031503074453300237060ustar00rootroot00000000000000#ifndef GAME_SOUND_SOUND_DECODER_H #define GAME_SOUND_SOUND_DECODER_H #include #include #include namespace VFS { class Manager; } namespace MWSound { enum SampleType { SampleType_UInt8, SampleType_Int16, SampleType_Float32 }; const char* getSampleTypeName(SampleType type); enum ChannelConfig { ChannelConfig_Mono, ChannelConfig_Stereo, ChannelConfig_Quad, ChannelConfig_5point1, ChannelConfig_7point1 }; 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); struct Sound_Decoder { const VFS::Manager* mResourceMgr; 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 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() {} private: Sound_Decoder(const Sound_Decoder& rhs); Sound_Decoder& operator=(const Sound_Decoder& rhs); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/sound_output.hpp000066400000000000000000000056231503074453300236530ustar00rootroot00000000000000#ifndef GAME_SOUND_SOUND_OUTPUT_H #define GAME_SOUND_SOUND_OUTPUT_H #include #include #include #include #include #include "../mwbase/soundmanager.hpp" namespace MWSound { class SoundManager; struct Sound_Decoder; class Sound; class Stream; // An opaque handle for the implementation's sound buffers. typedef void* Sound_Handle; // An opaque handle for the implementation's sound instances. typedef void* Sound_Instance; enum Environment { Env_Normal, Env_Underwater }; using HrtfMode = Settings::HrtfMode; class Sound_Output { SoundManager& mManager; virtual std::vector enumerate() = 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 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 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 pauseSounds(int types) = 0; virtual void resumeSounds(int types) = 0; virtual void pauseActiveDevice() = 0; virtual void resumeActiveDevice() = 0; Sound_Output& operator=(const Sound_Output& rhs); Sound_Output(const Sound_Output& rhs); protected: bool mInitialized; Sound_Output(SoundManager& mgr) : mManager(mgr) , mInitialized(false) { } public: virtual ~Sound_Output() {} bool isInitialized() const { return mInitialized; } friend class OpenAL_Output; friend class SoundManager; friend class SoundBufferPool; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/soundmanagerimp.cpp000066400000000000000000001245771503074453300243010ustar00rootroot00000000000000#include "soundmanagerimp.hpp" #include #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 "../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" 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() { WaterSoundUpdaterSettings settings; settings.mNearWaterRadius = Fallback::Map::getInt("Water_NearWaterRadius"); settings.mNearWaterPoints = Fallback::Map::getInt("Water_NearWaterPoints"); settings.mNearWaterIndoorTolerance = Fallback::Map::getFloat("Water_NearWaterIndoorTolerance"); settings.mNearWaterOutdoorTolerance = Fallback::Map::getFloat("Water_NearWaterOutdoorTolerance"); 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); } SoundManager::SoundManager(const VFS::Manager* vfs, bool useSound) : mVFS(vfs) , mOutput(std::make_unique(*this)) , mWaterSoundUpdater(makeWaterSoundUpdaterSettings()) , mSoundBuffers(*mOutput) , mMusicType(MWSound::MusicType::Normal) , mListenerUnderwater(false) , mListenerPos(0, 0, 0) , mListenerDir(1, 0, 0) , mListenerUp(0, 0, 1) , mUnderwaterSound(nullptr) , mNearWaterSound(nullptr) , mPlaybackPaused(false) , mTimePassed(0.f) , mLastCell(nullptr) , mCurrentRegionSound(nullptr) { if (!useSound) { Log(Debug::Info) << "Sound disabled."; return; } if (!mOutput->init(Settings::sound().mDevice, Settings::sound().mHrtf, Settings::sound().mHrtfEnable)) { Log(Debug::Error) << "Failed to initialize audio output, sound disabled"; return; } std::vector names = mOutput->enumerate(); std::stringstream stream; stream << "Enumerated output devices:\n"; for (const std::string& name : names) stream << " " << name; Log(Debug::Info) << stream.str(); stream.str(""); names = mOutput->enumerateHrtf(); if (!names.empty()) { stream << "Enumerated HRTF names:\n"; for (const std::string& name : names) stream << " " << name; Log(Debug::Info) << stream.str(); } } SoundManager::~SoundManager() { SoundManager::clear(); mSoundBuffers.clear(); mOutput.reset(); } // Return a new decoder instance, used as needed by the output implementations DecoderPtr SoundManager::getDecoder() { return std::make_shared(mVFS); } DecoderPtr SoundManager::loadVoice(VFS::Path::NormalizedView voicefile) { try { DecoderPtr decoder = getDecoder(); decoder->open(Misc::ResourceHelpers::correctSoundPath(voicefile, *decoder->mResourceMgr)); return decoder; } catch (std::exception& e) { Log(Debug::Error) << "Failed to load audio from " << voicefile << ": " << e.what(); } return nullptr; } SoundPtr SoundManager::getSoundRef() { return mSounds.get(); } StreamPtr SoundManager::getStreamRef() { return mStreams.get(); } 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 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) { sound->init([&] { SoundParams params; params.mBaseVolume = basevol; params.mFlags = PlayMode::NoEnv | Type::Voice | Play_2D; return params; }()); played = mOutput->streamSound(std::move(decoder), sound.get(), true); } else { sound->init([&] { SoundParams params; params.mPos = pos; params.mBaseVolume = basevol; params.mMinDistance = minDistance; params.mMaxDistance = maxDistance; params.mFlags = PlayMode::Normal | Type::Voice | Play_3D; return params; }()); played = mOutput->streamSound3D(std::move(decoder), sound.get(), true); } if (!played) return nullptr; return sound; } void SoundManager::stopMusic() { if (mMusic) { mOutput->finishStream(mMusic.get()); mMusic = nullptr; } } void SoundManager::streamMusicFull(VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; stopMusic(); if (filename.value().empty()) return; Log(Debug::Info) << "Playing \"" << filename << "\""; DecoderPtr decoder = getDecoder(); 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::NoEnvNoScaling | Type::Music | Play_2D; return params; }()); mOutput->streamSound(std::move(decoder), mMusic.get()); } void SoundManager::advanceMusic(VFS::Path::NormalizedView filename, float fadeOut) { if (!isMusicPlaying()) { streamMusicFull(filename); return; } mNextMusic = filename; mMusic->setFadeout(fadeOut); } bool SoundManager::isMusicPlaying() { return mMusic && mOutput->isStreamPlaying(mMusic.get()); } void SoundManager::streamMusic(VFS::Path::NormalizedView filename, MusicType type, float fade) { // Can not interrupt scripted music by built-in playlists if (mMusicType == MusicType::MWScript && type != MusicType::MWScript) return; mMusicType = type; advanceMusic(filename, fade); } void SoundManager::say(const MWWorld::ConstPtr& ptr, VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; DecoderPtr decoder = loadVoice(filename); if (!decoder) return; MWBase::World* world = MWBase::Environment::get().getWorld(); const osg::Vec3f pos = world->getActorHeadTransform(ptr).getTrans(); stopSay(ptr); StreamPtr sound = playVoice(std::move(decoder), pos, (ptr == MWMechanics::getPlayer())); if (!sound) return; mSaySoundsQueue.emplace(ptr.mRef, SaySound{ ptr.mCell, std::move(sound) }); } float SoundManager::getSaySoundLoudness(const MWWorld::ConstPtr& ptr) const { SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); if (snditer != mActiveSaySounds.end()) { Stream* sound = snditer->second.mStream.get(); return mOutput->getStreamLoudness(sound); } return 0.0f; } void SoundManager::say(VFS::Path::NormalizedView filename) { if (!mOutput->isInitialized()) return; DecoderPtr decoder = loadVoice(filename); if (!decoder) return; stopSay(MWWorld::ConstPtr()); StreamPtr sound = playVoice(std::move(decoder), osg::Vec3f(), true); if (!sound) return; mActiveSaySounds.emplace(nullptr, SaySound{ nullptr, std::move(sound) }); } bool SoundManager::sayDone(const MWWorld::ConstPtr& ptr) const { SaySoundMap::const_iterator snditer = mActiveSaySounds.find(ptr.mRef); if (snditer != mActiveSaySounds.end()) { if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return false; return true; } return true; } bool SoundManager::sayActive(const MWWorld::ConstPtr& ptr) const { SaySoundMap::const_iterator snditer = mSaySoundsQueue.find(ptr.mRef); if (snditer != mSaySoundsQueue.end()) { if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } snditer = mActiveSaySounds.find(ptr.mRef); if (snditer != mActiveSaySounds.end()) { if (mOutput->isStreamPlaying(snditer->second.mStream.get())) return true; return false; } return false; } void SoundManager::stopSay(const MWWorld::ConstPtr& ptr) { SaySoundMap::iterator snditer = mSaySoundsQueue.find(ptr.mRef); if (snditer != mSaySoundsQueue.end()) { mOutput->finishStream(snditer->second.mStream.get()); mSaySoundsQueue.erase(snditer); } snditer = mActiveSaySounds.find(ptr.mRef); if (snditer != mActiveSaySounds.end()) { mOutput->finishStream(snditer->second.mStream.get()); mActiveSaySounds.erase(snditer); } } Stream* SoundManager::playTrack(const DecoderPtr& decoder, Type type) { if (!mOutput->isInitialized()) return nullptr; StreamPtr track = getStreamRef(); track->init([&] { SoundParams params; params.mBaseVolume = volumeFromType(type); params.mFlags = PlayMode::NoEnvNoScaling | type | Play_2D; return params; }()); if (!mOutput->streamSound(decoder, track.get())) return nullptr; Stream* result = track.get(); const auto it = std::lower_bound(mActiveTracks.begin(), mActiveTracks.end(), track); mActiveTracks.insert(it, std::move(track)); return result; } 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) mActiveTracks.erase(iter); } 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; return false; } Sound* SoundManager::playSound(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, so stop previous copy stopSound(sfx, MWWorld::ConstPtr()); SoundPtr sound = getSoundRef(); sound->init([&] { SoundParams params; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; }()); if (!mOutput->playSound(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } Sound* SoundManager::playSound( std::string_view fileName, float volume, float pitch, Type type, PlayMode mode, float offset) { if (!mOutput->isInitialized()) return nullptr; std::string normalizedName = VFS::Path::normalizeFilename(fileName); if (!mVFS->exists(normalizedName)) 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()) { sound->init([&] { SoundParams params; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mPitch = pitch; params.mFlags = mode | type | Play_2D; return params; }()); played = mOutput->playSound(sound.get(), sfx->getHandle(), offset); } else { sound->init([&] { SoundParams params; 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) return nullptr; Sound* result = sound.get(); 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 MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float volume, float pitch, Type type, PlayMode mode, float offset) { if (remove3DSoundAtDistance(mode, ptr)) return nullptr; // Look up the sound in the ESM data 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([&] { SoundParams params; params.mPos = initialPos; params.mVolume = volume * sfx->getVolume(); params.mBaseVolume = volumeFromType(type); params.mFadeVolume = initialFadeVolume(squaredDist, sfx, type, mode); params.mPitch = pitch; params.mMinDistance = sfx->getMinDist(); params.mMaxDistance = sfx->getMaxDist(); params.mFlags = mode | type | Play_3D; return params; }()); if (!mOutput->playSound3D(sound.get(), sfx->getHandle(), offset)) return nullptr; Sound* result = sound.get(); mActiveSounds[nullptr].mList.emplace_back(std::move(sound), sfx); mSoundBuffers.use(*sfx); return result; } void SoundManager::stopSound(Sound* sound) { if (sound) mOutput->finishSound(sound); } void SoundManager::stopSound(Sound_Buffer* sfx, const MWWorld::ConstPtr& ptr) { SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { for (SoundBufferRefPair& snd : snditer->second.mList) { if (snd.second == sfx) mOutput->finishSound(snd.first.get()); } } } 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()) 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) { SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { for (SoundBufferRefPair& snd : snditer->second.mList) mOutput->finishSound(snd.first.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) { for (auto& [ref, sound] : mActiveSounds) { if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) { for (SoundBufferRefPair& sndbuf : sound.mList) mOutput->finishSound(sndbuf.first.get()); } } for (const auto& [ref, sound] : mSaySoundsQueue) { if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) mOutput->finishStream(sound.mStream.get()); } for (const auto& [ref, sound] : mActiveSaySounds) { if (ref != nullptr && ref != MWMechanics::getPlayer().mRef && sound.mCell == cell) mOutput->finishStream(sound.mStream.get()); } } void SoundManager::fadeOutSound3D(const MWWorld::ConstPtr& ptr, const ESM::RefId& soundId, float duration) { SoundMap::iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { Sound_Buffer* sfx = mSoundBuffers.lookup(soundId); if (sfx == nullptr) return; for (SoundBufferRefPair& sndbuf : snditer->second.mList) { if (sndbuf.second == sfx) sndbuf.first->setFadeout(duration); } } } bool SoundManager::getSoundPlaying(const MWWorld::ConstPtr& ptr, std::string_view fileName) const { std::string normalizedName = VFS::Path::normalizeFilename(fileName); SoundMap::const_iterator snditer = mActiveSounds.find(ptr.mRef); if (snditer != mActiveSounds.end()) { 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 (mPausedSoundTypes[blocker] != 0) resumeSounds(blocker); types = types & Type::Mask; mOutput->pauseSounds(types); mPausedSoundTypes[blocker] = types; } } void SoundManager::resumeSounds(BlockerType blocker) { if (mOutput->isInitialized()) { mPausedSoundTypes[blocker] = 0; int types = int(Type::Mask); for (int currentBlocker = 0; currentBlocker < BlockerType::MaxCount; currentBlocker++) { if (currentBlocker != blocker) types &= ~mPausedSoundTypes[currentBlocker]; } mOutput->resumeSounds(types); } } void SoundManager::pausePlayback() { if (mPlaybackPaused) return; mPlaybackPaused = true; mOutput->pauseActiveDevice(); } void SoundManager::resumePlayback() { if (!mPlaybackPaused) return; mPlaybackPaused = false; mOutput->resumeActiveDevice(); } void SoundManager::updateRegionSound(float duration) { MWBase::World* world = MWBase::Environment::get().getWorld(); const MWWorld::ConstPtr player = world->getPlayerPtr(); auto cell = player.getCell()->getCell(); if (!cell->isExterior() && !cell->isQuasiExterior()) return; if (mCurrentRegionSound && mOutput->isSoundPlaying(mCurrentRegionSound)) return; 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 MWWorld::Cell* curcell = player.getCell()->getCell(); const auto update = mWaterSoundUpdater.update(player, *world); WaterSoundAction action; Sound_Buffer* sfx; std::tie(action, sfx) = getWaterSoundAction(update, curcell); switch (action) { case WaterSoundAction::DoNothing: break; case WaterSoundAction::SetVolume: mNearWaterSound->setVolume(update.mVolume * sfx->getVolume()); mNearWaterSound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); break; case WaterSoundAction::FinishSound: mNearWaterSound->setFade(sSfxFadeOutDuration, 0.0f, Play_FadeExponential | Play_StopAtFadeEnd); break; case WaterSoundAction::PlaySound: if (mNearWaterSound) mOutput->finishSound(mNearWaterSound); mNearWaterSound = playSound(update.mId, update.mVolume, 1.0f, Type::Sfx, PlayMode::Loop); break; } mLastCell = curcell; } std::pair SoundManager::getWaterSoundAction( const WaterSoundUpdate& update, const MWWorld::Cell* cell) const { if (mNearWaterSound) { if (update.mVolume == 0.0f) return { WaterSoundAction::FinishSound, nullptr }; bool soundIdChanged = false; Sound_Buffer* sfx = mSoundBuffers.lookup(update.mId); if (mLastCell != cell) { const auto snditer = mActiveSounds.find(nullptr); if (snditer != mActiveSounds.end()) { 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 }; if (sfx) return { WaterSoundAction::SetVolume, sfx }; } else if (update.mVolume > 0.0f) 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; const osg::Vec3f pos = sound->getPosition(); const float squaredDist = (mListenerPos - pos).length2(); if (squaredDist > squaredMaxDist) { // If getDistanceCull() is set, delete the sound after it has faded out sound->setFade( sSfxFadeOutDuration, 0.0f, Play_FadeExponential | (sound->getDistanceCull() ? Play_StopAtFadeEnd : 0)); } else { // Fade sounds back in once they are in range sound->setFade(sSfxFadeInDuration, 1.0f, Play_FadeExponential); } } void SoundManager::updateSounds(float duration) { // We update active say sounds map for specific actors here // because for vanilla compatibility we can't do it immediately. SaySoundMap::iterator queuesayiter = mSaySoundsQueue.begin(); while (queuesayiter != mSaySoundsQueue.end()) { const auto dst = mActiveSaySounds.find(queuesayiter->first); if (dst == mActiveSaySounds.end()) mActiveSaySounds.emplace(queuesayiter->first, std::move(queuesayiter->second)); else dst->second = std::move(queuesayiter->second); mSaySoundsQueue.erase(queuesayiter++); } mTimePassed += duration; if (mTimePassed < sMinUpdateInterval) return; duration = mTimePassed; mTimePassed = 0.0f; Environment env = Env_Normal; if (mListenerUnderwater) env = Env_Underwater; else if (mUnderwaterSound) { mOutput->finishSound(mUnderwaterSound); mUnderwaterSound = nullptr; } mOutput->startUpdate(); 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()) { MWWorld::ConstPtr ptr = snditer->first; SoundBufferRefPairList::iterator sndidx = snditer->second.mList.begin(); while (sndidx != snditer->second.mList.end()) { Sound* sound = sndidx->first.get(); if (sound->getIs3D()) { if (!ptr.isEmpty()) sound->setPosition(ptr.getRefData().getPosition().asVec3()); cull3DSound(sound); } if (!sound->updateFade(duration) || !mOutput->isSoundPlaying(sound)) { mOutput->finishSound(sound); if (sound == mUnderwaterSound) mUnderwaterSound = nullptr; if (sound == mNearWaterSound) mNearWaterSound = nullptr; mSoundBuffers.release(*sndidx->second); sndidx = snditer->second.mList.erase(sndidx); } else { mOutput->updateSound(sound); ++sndidx; } } if (snditer->second.mList.empty()) snditer = mActiveSounds.erase(snditer); else ++snditer; } SaySoundMap::iterator sayiter = mActiveSaySounds.begin(); while (sayiter != mActiveSaySounds.end()) { MWWorld::ConstPtr ptr = sayiter->first; Stream* sound = sayiter->second.mStream.get(); if (sound->getIs3D()) { if (!ptr.isEmpty()) { MWBase::World* world = MWBase::Environment::get().getWorld(); sound->setPosition(world->getActorHeadTransform(ptr).getTrans()); } cull3DSound(sound); } if (!sound->updateFade(duration) || !mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); sayiter = mActiveSaySounds.erase(sayiter); } else { mOutput->updateStream(sound); ++sayiter; } } TrackList::iterator trkiter = mActiveTracks.begin(); while (trkiter != mActiveTracks.end()) { Stream* sound = trkiter->get(); if (!mOutput->isStreamPlaying(sound)) { mOutput->finishStream(sound); trkiter = mActiveTracks.erase(trkiter); } else { sound->updateFade(duration); mOutput->updateStream(sound); ++trkiter; } } if (mListenerUnderwater) { // Play underwater sound (after updating sounds) if (!mUnderwaterSound) mUnderwaterSound = playSound(ESM::RefId::stringRefId("Underwater"), 1.0f, 1.0f, Type::Sfx, PlayMode::LoopNoEnv); } mOutput->finishUpdate(); } void SoundManager::updateMusic(float duration) { if (!mMusic || !mMusic->updateFade(duration) || !mOutput->isStreamPlaying(mMusic.get())) { stopMusic(); if (!mNextMusic.value().empty()) { streamMusicFull(mNextMusic); mNextMusic = VFS::Path::Normalized(); } else mMusicType = MusicType::Normal; } else { mOutput->updateStream(mMusic.get()); } } void SoundManager::update(float duration) { 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 (state != MWBase::StateManager::State_NoGame) { updateRegionSound(duration); updateWaterSound(); } } void SoundManager::processChangedSettings(const Settings::CategorySettingVector& settings) { if (!mOutput->isInitialized()) return; mOutput->startUpdate(); for (SoundMap::value_type& snd : mActiveSounds) { for (SoundBufferRefPair& sndbuf : snd.second.mList) { Sound* sound = sndbuf.first.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateSound(sound); } } for (SaySoundMap::value_type& snd : mActiveSaySounds) { Stream* sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } for (SaySoundMap::value_type& snd : mSaySoundsQueue) { Stream* sound = snd.second.mStream.get(); sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound); } for (const StreamPtr& sound : mActiveTracks) { sound->setBaseVolume(volumeFromType(sound->getPlayType())); mOutput->updateStream(sound.get()); } if (mMusic) { mMusic->setBaseVolume(volumeFromType(mMusic->getPlayType())); mOutput->updateStream(mMusic.get()); } mOutput->finishUpdate(); } void SoundManager::setListenerPosDir( const osg::Vec3f& pos, const osg::Vec3f& dir, const osg::Vec3f& up, bool underwater) { mListenerPos = pos; mListenerDir = dir; mListenerUp = up; mListenerUnderwater = underwater; mWaterSoundUpdater.setUnderwater(underwater); } void SoundManager::updatePtr(const MWWorld::ConstPtr& old, const MWWorld::ConstPtr& updated) { SoundMap::iterator snditer = mActiveSounds.find(old.mRef); if (snditer != mActiveSounds.end()) snditer->second.mCell = updated.mCell; if (const auto it = mSaySoundsQueue.find(old.mRef); it != mSaySoundsQueue.end()) it->second.mCell = updated.mCell; 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) { size_t total = output.size(); size_t got; output.resize(total + 32768); while ((got = read(&output[total], output.size() - total)) > 0) { total += got; output.resize(total * 2); } output.resize(total); } const char* getSampleTypeName(SampleType type) { switch (type) { case SampleType_UInt8: return "U8"; case SampleType_Int16: return "S16"; case SampleType_Float32: return "Float32"; } return "(unknown sample type)"; } const char* getChannelConfigName(ChannelConfig 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"; } return "(unknown channel config)"; } size_t framesToBytes(size_t frames, ChannelConfig config, SampleType type) { 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; } switch (type) { case SampleType_UInt8: frames *= 1; break; case SampleType_Int16: frames *= 2; break; case SampleType_Float32: frames *= 4; break; } return frames; } size_t bytesToFrames(size_t bytes, ChannelConfig config, SampleType type) { return bytes / framesToBytes(1, config, type); } void SoundManager::clear() { stopMusic(); mMusicType = MusicType::Normal; for (SoundMap::value_type& snd : mActiveSounds) { for (SoundBufferRefPair& sndbuf : snd.second.mList) { mOutput->finishSound(sndbuf.first.get()); mSoundBuffers.release(*sndbuf.second); } } mActiveSounds.clear(); mUnderwaterSound = nullptr; mNearWaterSound = nullptr; for (SaySoundMap::value_type& snd : mSaySoundsQueue) mOutput->finishStream(snd.second.mStream.get()); mSaySoundsQueue.clear(); for (SaySoundMap::value_type& snd : mActiveSaySounds) mOutput->finishStream(snd.second.mStream.get()); mActiveSaySounds.clear(); for (StreamPtr& sound : mActiveTracks) mOutput->finishStream(sound.get()); mActiveTracks.clear(); mPlaybackPaused = false; std::fill(std::begin(mPausedSoundTypes), std::end(mPausedSoundTypes), 0); } } openmw-openmw-0.49.0/apps/openmw/mwsound/soundmanagerimp.hpp000066400000000000000000000251521503074453300242730ustar00rootroot00000000000000#ifndef GAME_SOUND_SOUNDMANAGER_H #define GAME_SOUND_SOUNDMANAGER_H #include #include #include #include #include #include #include #include #include #include "../mwbase/soundmanager.hpp" #include "regionsoundselector.hpp" #include "sound_buffer.hpp" #include "type.hpp" #include "watersoundupdater.hpp" namespace VFS { class Manager; } namespace ESM { struct Sound; struct Cell; } namespace MWWorld { class Cell; } namespace MWSound { class Sound_Output; struct Sound_Decoder; class SoundBase; class Sound; class Stream; using SoundPtr = Misc::ObjectPtr; using StreamPtr = Misc::ObjectPtr; class SoundManager : public MWBase::SoundManager { const VFS::Manager* mVFS; std::unique_ptr mOutput; WaterSoundUpdater mWaterSoundUpdater; SoundBufferPool mSoundBuffers; Misc::ObjectPool mSounds; Misc::ObjectPool mStreams; typedef std::pair SoundBufferRefPair; typedef std::vector SoundBufferRefPairList; struct ActiveSound { const MWWorld::CellStore* mCell = nullptr; SoundBufferRefPairList mList; }; typedef std::map SoundMap; SoundMap mActiveSounds; struct SaySound { const MWWorld::CellStore* mCell; StreamPtr mStream; }; typedef std::map SaySoundMap; SaySoundMap mSaySoundsQueue; SaySoundMap mActiveSaySounds; typedef std::vector TrackList; TrackList mActiveTracks; StreamPtr mMusic; MusicType mMusicType; bool mListenerUnderwater; osg::Vec3f mListenerPos; osg::Vec3f mListenerDir; osg::Vec3f mListenerUp; int mPausedSoundTypes[BlockerType::MaxCount] = {}; Sound* mUnderwaterSound; Sound* mNearWaterSound; VFS::Path::Normalized mNextMusic; bool mPlaybackPaused; RegionSoundSelector mRegionSoundSelector; float mTimePassed; const MWWorld::Cell* mLastCell; Sound* mCurrentRegionSound; 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(VFS::Path::NormalizedView voicefile); SoundPtr getSoundRef(); StreamPtr getStreamRef(); 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 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); enum class WaterSoundAction { DoNothing, SetVolume, FinishSound, PlaySound, }; std::pair getWaterSoundAction( const WaterSoundUpdate& update, const MWWorld::Cell* cell) const; 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); ///< Stop the given object from playing given sound buffer. public: SoundManager(const VFS::Manager* vfs, bool useSound); ~SoundManager() override; 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 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 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 say(const MWWorld::ConstPtr& reference, VFS::Path::NormalizedView filename) override; ///< Make an actor say some text. /// \param filename name of a sound file in the VFS void say(VFS::Path::NormalizedView filename) override; ///< Say some text, without an actor ref /// \param filename name of a sound file in the VFS bool sayActive(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const override; ///< Is actor not speaking? bool sayDone(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) const override; ///< For scripting backward compatibility void stopSay(const MWWorld::ConstPtr& reference = MWWorld::ConstPtr()) override; ///< Stop an actor speaking float getSaySoundLoudness(const MWWorld::ConstPtr& reference) const override; ///< 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. Stream* playTrack(const DecoderPtr& decoder, Type type) override; ///< Play a 2D audio track, using a custom decoder void stopTrack(Stream* stream) override; ///< Stop the given audio track from playing 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 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 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 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. 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 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; ///< Stop the given object from playing all sounds. void stopSound(const MWWorld::CellStore* cell) override; ///< Stop all sounds for the given cell. 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 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; ///< Pauses all currently playing sounds, including music. void resumeSounds(MWSound::BlockerType blocker) override; ///< Resumes all previously paused sounds. void pausePlayback() override; void resumePlayback() override; void update(float duration); 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 clear() override; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/type.hpp000066400000000000000000000006031503074453300220550ustar00rootroot00000000000000#ifndef GAME_SOUND_TYPE_H #define GAME_SOUND_TYPE_H namespace MWSound { enum class Type { 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 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwsound/watersoundupdater.cpp000066400000000000000000000046771503074453300246660ustar00rootroot00000000000000#include "watersoundupdater.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/ptr.hpp" #include #include namespace MWSound { WaterSoundUpdater::WaterSoundUpdater(const WaterSoundUpdaterSettings& settings) : mSettings(settings) { } WaterSoundUpdate WaterSoundUpdater::update(const MWWorld::ConstPtr& player, const MWBase::World& world) const { WaterSoundUpdate result; result.mId = player.getCell()->isExterior() ? mSettings.mNearWaterOutdoorID : mSettings.mNearWaterIndoorID; result.mVolume = std::min(1.0f, getVolume(player, world)); return result; } float WaterSoundUpdater::getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const { if (mListenerUnderwater) return 1.0f; const MWWorld::CellStore& cell = *player.getCell(); if (!cell.getCell()->hasWater()) return 0.0f; const osg::Vec3f pos = player.getRefData().getPosition().asVec3(); const float dist = std::abs(cell.getWaterLevel() - pos.z()); if (cell.isExterior() && dist < mSettings.mNearWaterOutdoorTolerance) { if (mSettings.mNearWaterPoints <= 1) return (mSettings.mNearWaterOutdoorTolerance - dist) / mSettings.mNearWaterOutdoorTolerance; const float step = mSettings.mNearWaterRadius * 2.0f / (mSettings.mNearWaterPoints - 1); int underwaterPoints = 0; for (int x = 0; x < mSettings.mNearWaterPoints; x++) { for (int y = 0; y < mSettings.mNearWaterPoints; y++) { 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), cell.getCell()->getWorldSpace()); if (height < 0) underwaterPoints++; } } return underwaterPoints * 2.0f / (mSettings.mNearWaterPoints * mSettings.mNearWaterPoints); } if (!cell.isExterior() && dist < mSettings.mNearWaterIndoorTolerance) return (mSettings.mNearWaterIndoorTolerance - dist) / mSettings.mNearWaterIndoorTolerance; return 0.0f; } } openmw-openmw-0.49.0/apps/openmw/mwsound/watersoundupdater.hpp000066400000000000000000000021321503074453300246530ustar00rootroot00000000000000#ifndef GAME_SOUND_WATERSOUNDUPDATER_H #define GAME_SOUND_WATERSOUNDUPDATER_H #include "components/esm/refid.hpp" #include namespace MWBase { class World; } namespace MWWorld { class ConstPtr; } namespace MWSound { struct WaterSoundUpdaterSettings { int mNearWaterRadius; int mNearWaterPoints; float mNearWaterIndoorTolerance; float mNearWaterOutdoorTolerance; ESM::RefId mNearWaterIndoorID; ESM::RefId mNearWaterOutdoorID; }; struct WaterSoundUpdate { ESM::RefId mId; float mVolume; }; class WaterSoundUpdater { public: explicit WaterSoundUpdater(const WaterSoundUpdaterSettings& settings); WaterSoundUpdate update(const MWWorld::ConstPtr& player, const MWBase::World& world) const; void setUnderwater(bool value) { mListenerUnderwater = value; } private: const WaterSoundUpdaterSettings mSettings; bool mListenerUnderwater = false; float getVolume(const MWWorld::ConstPtr& player, const MWBase::World& world) const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwstate/000077500000000000000000000000001503074453300203545ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwstate/character.cpp000066400000000000000000000125171503074453300230220ustar00rootroot00000000000000#include "character.hpp" #include #include #include #include #include #include #include #include #include #include #include bool MWState::operator<(const Slot& left, const Slot& right) { return left.mTimeStamp < right.mTimeStamp; } std::string MWState::getFirstGameFile(const std::vector& contentFiles) { for (const std::string& c : contentFiles) { if (Misc::StringUtils::ciEndsWith(c, ".esm") || Misc::StringUtils::ciEndsWith(c, ".omwgame")) return c; } return ""; } void MWState::Character::addSlot(const std::filesystem::path& path, const std::string& game) { Slot slot; slot.mPath = path; slot.mTimeStamp = std::filesystem::last_write_time(path); ESM::ESMReader reader; reader.open(slot.mPath); if (reader.getRecName() != ESM::REC_SAVE) return; // invalid save file -> ignore reader.getRecHeader(); slot.mProfile.load(reader); if (!Misc::StringUtils::ciEqual(getFirstGameFile(slot.mProfile.mContentFiles), game)) return; // this file is for a different game -> ignore mSlots.push_back(slot); } 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 Utf8Stream description(profile.mDescription); while (!description.eof()) { auto c = description.consume(); if (c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters stream << static_cast(c); else 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 (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::filesystem::file_time_type::clock::now(); mSlots.push_back(slot); } MWState::Character::Character(const std::filesystem::path& saves, const std::string& game) : mPath(saves) { if (!std::filesystem::is_directory(mPath)) { std::filesystem::create_directories(mPath); } else { for (const auto& iter : std::filesystem::directory_iterator(mPath)) { try { addSlot(iter, game); } catch (const std::exception& e) { Log(Debug::Warning) << "Failed to add slot for game \"" << game << "\" save " << iter << ": " << e.what(); } } std::sort(mSlots.begin(), mSlots.end()); } } void MWState::Character::cleanup() { if (mSlots.empty()) { // All slots are gone, no need to keep the empty directory if (std::filesystem::is_directory(mPath)) { // Extra safety check to make sure the directory is empty (e.g. slots failed to parse header) 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) { addSlot(profile); return &mSlots.back(); } void MWState::Character::deleteSlot(const Slot* slot) { std::ptrdiff_t index = slot - mSlots.data(); if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable throw std::logic_error("slot not found"); } std::filesystem::remove(slot->mPath); mSlots.erase(mSlots.begin() + index); } const MWState::Slot* MWState::Character::updateSlot(const Slot* slot, const ESM::SavedGame& profile) { std::ptrdiff_t index = slot - mSlots.data(); if (index < 0 || static_cast(index) >= mSlots.size()) { // sanity check; not entirely reliable throw std::logic_error("slot not found"); } Slot newSlot = *slot; newSlot.mProfile = profile; newSlot.mTimeStamp = std::filesystem::file_time_type::clock::now(); mSlots.erase(mSlots.begin() + index); mSlots.push_back(newSlot); return &mSlots.back(); } MWState::Character::SlotIterator MWState::Character::begin() const { return mSlots.rbegin(); } MWState::Character::SlotIterator MWState::Character::end() const { return mSlots.rend(); } const ESM::SavedGame& MWState::Character::getSignature() const { if (mSlots.empty()) throw std::logic_error("character signature not available"); const auto tiePlayerLevelAndTimeStamp = [](const Slot& v) { return std::tie(v.mProfile.mPlayerLevel, v.mTimeStamp); }; const auto lessByPlayerLevelAndTimeStamp = [&](const Slot& l, const Slot& r) { return tiePlayerLevelAndTimeStamp(l) < tiePlayerLevelAndTimeStamp(r); }; return std::max_element(mSlots.begin(), mSlots.end(), lessByPlayerLevelAndTimeStamp)->mProfile; } const std::filesystem::path& MWState::Character::getPath() const { return mPath; } openmw-openmw-0.49.0/apps/openmw/mwstate/character.hpp000066400000000000000000000036661503074453300230340ustar00rootroot00000000000000#ifndef GAME_STATE_CHARACTER_H #define GAME_STATE_CHARACTER_H #include #include namespace MWState { struct Slot { std::filesystem::path mPath; ESM::SavedGame mProfile; std::filesystem::file_time_type mTimeStamp; }; 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: std::filesystem::path mPath; std::vector mSlots; void addSlot(const std::filesystem::path& path, const std::string& game); void addSlot(const ESM::SavedGame& profile); public: Character(const std::filesystem::path& saves, const std::string& game); 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. /// \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. SlotIterator begin() const; ///< Any call to createSlot and updateSlot can invalidate the returned iterator. SlotIterator end() const; const std::filesystem::path& getPath() const; const ESM::SavedGame& getSignature() const; ///< Return signature information for this character. /// /// \attention This function must not be called if there are no slots. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwstate/charactermanager.cpp000066400000000000000000000063661503074453300243620ustar00rootroot00000000000000#include "charactermanager.hpp" #include #include #include #include #include MWState::CharacterManager::CharacterManager(std::filesystem::path saves, const std::vector& contentFiles) : mPath(std::move(saves)) , mCurrent(nullptr) , mGame(getFirstGameFile(contentFiles)) { if (!std::filesystem::is_directory(mPath)) { std::filesystem::create_directories(mPath); } else { for (std::filesystem::directory_iterator iter(mPath); iter != std::filesystem::directory_iterator(); ++iter) { std::filesystem::path characterDir = *iter; if (std::filesystem::is_directory(characterDir)) { Character character(characterDir, mGame); if (character.begin() != character.end()) mCharacters.push_back(character); } } } } MWState::Character* MWState::CharacterManager::getCurrentCharacter() { return mCurrent; } void MWState::CharacterManager::deleteSlot(const MWState::Slot* slot, const MWState::Character*& character) { std::list::iterator it = findCharacter(character); it->deleteSlot(slot); if (character->begin() == character->end()) { // All slots deleted, cleanup and remove this character it->cleanup(); if (character == mCurrent) mCurrent = nullptr; mCharacters.erase(it); character = nullptr; } } MWState::Character* MWState::CharacterManager::createCharacter(const std::string& name) { std::ostringstream stream; // The character name is user-supplied, so we need to escape the path Utf8Stream nameStream(name); while (!nameStream.eof()) { auto c = nameStream.consume(); if (c <= 0x7F && std::isalnum(c)) // Ignore multibyte characters and non alphanumeric characters stream << static_cast(c); else stream << '_'; } std::filesystem::path path = mPath / stream.str(); // Append an index if necessary to ensure a unique directory int i = 0; while (std::filesystem::exists(path)) { std::ostringstream test; test << stream.str(); test << " - " << ++i; path = mPath / test.str(); } mCharacters.emplace_back(path, mGame); return &mCharacters.back(); } std::list::iterator MWState::CharacterManager::findCharacter(const MWState::Character* character) { std::list::iterator it = mCharacters.begin(); for (; it != mCharacters.end(); ++it) { if (&*it == character) break; } if (it == mCharacters.end()) throw std::logic_error("invalid character"); return it; } void MWState::CharacterManager::setCurrentCharacter(const Character* character) { if (!character) mCurrent = nullptr; else { std::list::iterator it = findCharacter(character); mCurrent = &*it; } } std::list::const_iterator MWState::CharacterManager::begin() const { return mCharacters.begin(); } std::list::const_iterator MWState::CharacterManager::end() const { return mCharacters.end(); } openmw-openmw-0.49.0/apps/openmw/mwstate/charactermanager.hpp000066400000000000000000000025311503074453300243550ustar00rootroot00000000000000#ifndef GAME_STATE_CHARACTERMANAGER_H #define GAME_STATE_CHARACTERMANAGER_H #include #include #include "character.hpp" namespace MWState { class CharacterManager { std::filesystem::path mPath; // Uses std::list, so that mCurrent stays valid when characters are deleted std::list mCharacters; Character* mCurrent; std::string mGame; private: CharacterManager(const CharacterManager&); ///< Not implemented CharacterManager& operator=(const CharacterManager&); ///< Not implemented std::list::iterator findCharacter(const MWState::Character* character); public: CharacterManager(std::filesystem::path saves, const std::vector& contentFiles); Character* getCurrentCharacter(); ///< @note May return null void deleteSlot(const MWState::Slot* slot, 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) void setCurrentCharacter(const Character* character); std::list::const_iterator begin() const; std::list::const_iterator end() const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwstate/quicksavemanager.cpp000066400000000000000000000016631503074453300244140ustar00rootroot00000000000000#include "quicksavemanager.hpp" MWState::QuickSaveManager::QuickSaveManager(std::string& saveName, unsigned int maxSaves) : mSaveName(saveName) , mMaxSaves(maxSaves) , mSlotsVisited(0) , mOldestSlotVisited(nullptr) { } void MWState::QuickSaveManager::visitSave(const Slot* saveSlot) { if (mSaveName == saveSlot->mProfile.mDescription) { ++mSlotsVisited; if (isOldestSave(saveSlot)) mOldestSlotVisited = saveSlot; } } bool MWState::QuickSaveManager::isOldestSave(const Slot* compare) const { if (mOldestSlotVisited == nullptr) return true; return (compare->mTimeStamp <= mOldestSlotVisited->mTimeStamp); } bool MWState::QuickSaveManager::shouldCreateNewSlot() const { return (mSlotsVisited < mMaxSaves); } const MWState::Slot* MWState::QuickSaveManager::getNextQuickSaveSlot() { if (shouldCreateNewSlot()) return nullptr; return mOldestSlotVisited; } openmw-openmw-0.49.0/apps/openmw/mwstate/quicksavemanager.hpp000066400000000000000000000020741503074453300244160ustar00rootroot00000000000000#ifndef GAME_STATE_QUICKSAVEMANAGER_H #define GAME_STATE_QUICKSAVEMANAGER_H #include #include "character.hpp" namespace MWState { class QuickSaveManager { std::string mSaveName; unsigned int mMaxSaves; unsigned int mSlotsVisited; const Slot* mOldestSlotVisited; private: bool shouldCreateNewSlot() const; bool isOldestSave(const Slot* compare) const; public: 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); ///< Visits the given \a slot \a 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 }; } #endif openmw-openmw-0.49.0/apps/openmw/mwstate/statemanagerimp.cpp000066400000000000000000000747441503074453300242610ustar00rootroot00000000000000#include "statemanagerimp.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwbase/dialoguemanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/inputmanager.hpp" #include "../mwbase/journal.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/datetimemanager.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/globals.hpp" #include "../mwworld/scene.hpp" #include "../mwworld/worldmodel.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwscript/globalscripts.hpp" #include "quicksavemanager.hpp" void MWState::StateManager::cleanup(bool force) { if (mState != State_NoGame || force) { MWBase::Environment::get().getSoundManager()->clear(); MWBase::Environment::get().getDialogueManager()->clear(); MWBase::Environment::get().getJournal()->clear(); MWBase::Environment::get().getScriptManager()->clear(); MWBase::Environment::get().getWindowManager()->clear(); MWBase::Environment::get().getWorld()->clear(); MWBase::Environment::get().getInputManager()->clear(); MWBase::Environment::get().getMechanicsManager()->clear(); 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 { const std::vector& current = MWBase::Environment::get().getWorld()->getContentFiles(); const std::vector& prev = reader.getGameFiles(); std::map map; for (int iPrev = 0; iPrev < static_cast(prev.size()); ++iPrev) { 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)); break; } } return map; } 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() { mQuitRequest = true; } bool MWState::StateManager::hasQuitRequest() const { return mQuitRequest; } void MWState::StateManager::askLoadRecent() { if (MWBase::Environment::get().getWindowManager()->getMode() == MWGui::GM_MainMenu) return; if (!mAskLoadRecent) { if (mLastSavegame.empty()) // no saves { MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } else { 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("#{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(), saveName); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons); mAskLoadRecent = true; } } } MWState::StateManager::State MWState::StateManager::getState() const { return mState; } void MWState::StateManager::newGame(bool bypass) { cleanup(); if (!bypass) 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); mState = State_Running; MWBase::Environment::get().getLuaManager()->gameLoaded(); MWBase::Environment::get().getWindowManager()->fadeScreenOut(0); MWBase::Environment::get().getWindowManager()->fadeScreenIn(1); } catch (std::exception& e) { std::stringstream error; error << "Failed to start new game: " << e.what(); Log(Debug::Error) << error.str(); cleanup(true); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); std::vector buttons; buttons.emplace_back("#{Interface:OK}"); MWBase::Environment::get().getWindowManager()->interactiveMessageBox(error.str(), buttons); } } 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(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(); const std::string& name = player.get()->mBase->mName; character = mCharacterManager.createCharacter(name); mCharacterManager.setCurrentCharacter(character); } ESM::SavedGame profile; MWBase::World& world = *MWBase::Environment::get().getWorld(); MWWorld::Ptr player = world.getPlayerPtr(); profile.mContentFiles = world.getContentFiles(); profile.mPlayerName = player.get()->mBase->mName; profile.mPlayerLevel = player.getClass().getNpcStats(player).getLevel(); 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; 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 << "'"; writeScreenshot(profile.mScreenshot); if (!slot) slot = character->createSlot(profile); else 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(); Log(Debug::Info) << "Writing saved game '" << description << "' for character '" << profile.mPlayerName << "'"; // Write to a memory stream first. If there is an exception during the save process, we don't want to trash the // existing save file we are overwriting. std::stringstream stream; ESM::ESMWriter writer; 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.setFormatVersion(ESM::CurrentSaveGameFormatVersion); // all unused writer.setVersion(0); writer.setType(0); writer.setAuthor(""); writer.setDescription(""); 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); 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("#{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); // 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(); writer.close(); if (stream.fail()) throw std::runtime_error("Write operation failed (memory stream)"); // All good, write to file std::ofstream filestream(slot->mPath, std::ios::binary); filestream << stream.rdbuf(); if (filestream.fail()) throw std::runtime_error("Write operation failed (file stream)"); 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) { std::stringstream error; error << "Failed to save game: " << e.what(); Log(Debug::Error) << error.str(); std::vector buttons; 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 && !std::filesystem::exists(slot->mPath)) { character->deleteSlot(slot); character->cleanup(); } } } void MWState::StateManager::quickSave(std::string name) { 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("#{OMWEngine:SaveGameDenied}"); return; } 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 saveFinder.visitSave(&save); } } // 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::filesystem::path& filepath) { for (const auto& character : mCharacterManager) { for (const auto& slot : character) { if (slot.mPath == filepath) { loadGame(&character, slot.mPath); return; } } } MWState::Character* character = getCurrentCharacter(); loadGame(character, 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 " << filepath.filename(); ESM::ESMReader reader; reader.open(filepath); 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); reader.setContentFileMapping(&contentFileMap); MWBase::Environment::get().getLuaManager()->setContentFileMapping(contentFileMap); Loading::Listener& listener = *MWBase::Environment::get().getWindowManager()->getLoadingScreen(); listener.setProgressRange(100); listener.setLabel("#{OMWEngine:LoadingInProgress}"); Loading::ScopedLoad load(&listener); bool firstPersonCam = false; size_t total = reader.getFileSize(); int currentPercent = 0; while (reader.hasMoreRecs()) { ESM::NAME n = reader.getRecName(); reader.getRecHeader(); 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)) { 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 << "'"; } break; case ESM::REC_JOUR: case ESM::REC_QUES: MWBase::Environment::get().getJournal()->readRecord(reader, n.toInt()); break; case ESM::REC_DIAS: 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: case ESM::REC_CLOT: case ESM::REC_ENCH: case ESM::REC_NPC_: case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_GLOB: case ESM::REC_PLAY: case ESM::REC_CSTA: case ESM::REC_WTHR: case ESM::REC_DYNA: case ESM::REC_ACTC: case ESM::REC_PROJ: case ESM::REC_MPRJ: case ESM::REC_ENAB: case ESM::REC_LEVC: case ESM::REC_LEVI: case ESM::REC_LIGH: case ESM::REC_CREA: case ESM::REC_CONT: case ESM::REC_RAND: MWBase::Environment::get().getWorld()->readRecord(reader, n.toInt()); break; case ESM::REC_CAM_: reader.getHNT(firstPersonCam, "FIRS"); break; case ESM::REC_GSCR: MWBase::Environment::get().getScriptManager()->getGlobalScripts().readRecord(reader, n.toInt()); break; case ESM::REC_GMAP: case ESM::REC_KEYS: case ESM::REC_ASPL: case ESM::REC_MARK: MWBase::Environment::get().getWindowManager()->readRecord(reader, n.toInt()); break; case ESM::REC_DCOU: case ESM::REC_STLN: MWBase::Environment::get().getMechanicsManager()->readRecord(reader, n.toInt()); break; case ESM::REC_INPU: 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.toStringView(); reader.skipRecord(); } int progressPercent = static_cast(float(reader.getFileOffset()) / total * 100); if (progressPercent > currentPercent) { listener.increaseProgress(progressPercent - currentPercent); currentPercent = progressPercent; } } mCharacterManager.setCurrentCharacter(character); mState = State_Running; if (character) Settings::saves().mCharacter.set(Files::pathToUnicodeString(character->getPath().filename())); mLastSavegame = filepath; MWBase::Environment::get().getWindowManager()->setNewGame(false); MWBase::Environment::get().getWorld()->saveLoaded(); MWBase::Environment::get().getWorld()->setupPlayer(); 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(); MWWorld::ConstPtr ptr = MWMechanics::getPlayer(); if (ptr.isInCell()) { 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); } else { // Cell no longer exists (i.e. changed game files), choose a default cell 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] = 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()->getId(), pos, true, false); } MWBase::Environment::get().getWorld()->updateProjectilesCasters(); // Vanilla MW will restart startup scripts when a save game is loaded. This is unintuitive, // but some mods may be using it as a reload detector. MWBase::Environment::get().getScriptManager()->getGlobalScripts().addStartup(); // 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().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::string error = "#{OMWEngine:LoadingFailed}: " + std::string(e.what()); printSavegameFormatError(e.what(), error); } } void MWState::StateManager::printSavegameFormatError( const std::string& exceptionText, const std::string& messageBoxText) { Log(Debug::Error) << "Failed to load saved game: " << exceptionText; cleanup(true); 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 (currentCharacter->begin() == currentCharacter->end()) return; // 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) { const std::filesystem::path savePath = slot->mPath; mCharacterManager.deleteSlot(slot, character); if (mLastSavegame == savePath) { if (character != nullptr) mLastSavegame = character->begin()->mPath; else mLastSavegame.clear(); } } MWState::Character* MWState::StateManager::getCurrentCharacter() { return mCharacterManager.getCurrentCharacter(); } MWState::StateManager::CharacterIterator MWState::StateManager::characterBegin() { return mCharacterManager.begin(); } MWState::StateManager::CharacterIterator MWState::StateManager::characterEnd() { return mCharacterManager.end(); } void MWState::StateManager::update(float duration) { mTimePlayed += duration; // Note: It would be nicer to trigger this from InputManager, i.e. the very beginning of the frame update. if (mAskLoadRecent) { int iButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); MWState::Character* curCharacter = getCurrentCharacter(); if (iButton == 0 && curCharacter) { mAskLoadRecent = false; // 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) { mAskLoadRecent = false; MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); } } if (mNewGameRequest) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); newGame(); mNewGameRequest = false; } if (mLoadRequest) { MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_MainMenu); loadGame(*mLoadRequest); mLoadRequest = std::nullopt; } } bool MWState::StateManager::confirmLoading(const std::vector& missingFiles) const { 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 { for (size_t i = 0; i < cappedSize - 1; ++i) { message += missingFiles[i]; message += "\n"; } message += "..."; } message += l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast(missingFiles.size()) }); int selectedButton = -1; while (true) { 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 { int screenshotW = 259 * 2, screenshotH = 133 * 2; // *2 to get some nice antialiasing osg::ref_ptr screenshot(new osg::Image); MWBase::Environment::get().getWorld()->screenshot(screenshot.get(), screenshotW, screenshotH); osgDB::ReaderWriter* readerwriter = osgDB::Registry::instance()->getReaderWriterForExtension("jpg"); if (!readerwriter) { Log(Debug::Error) << "Error: Unable to write screenshot, can't find a jpg ReaderWriter"; return; } std::ostringstream ostream; osgDB::ReaderWriter::WriteResult result = readerwriter->writeImage(*screenshot, ostream); if (!result.success()) { Log(Debug::Error) << "Error: Unable to write screenshot: " << result.message() << " code " << result.status(); return; } std::string data = ostream.str(); imageData = std::vector(data.begin(), data.end()); } openmw-openmw-0.49.0/apps/openmw/mwstate/statemanagerimp.hpp000066400000000000000000000065001503074453300242470ustar00rootroot00000000000000#ifndef GAME_STATE_STATEMANAGER_H #define GAME_STATE_STATEMANAGER_H #include #include #include "../mwbase/statemanager.hpp" #include "charactermanager.hpp" namespace MWState { class StateManager : public MWBase::StateManager { bool mQuitRequest; bool mAskLoadRecent; bool mNewGameRequest = false; std::optional mLoadRequest; State mState; CharacterManager mCharacterManager; double mTimePlayed; std::filesystem::path mLastSavegame; private: void cleanup(bool force = false); void printSavegameFormatError(const std::string& exceptionText, const std::string& messageBoxText); bool confirmLoading(const std::vector& missingFiles) const; void writeScreenshot(std::vector& imageData) const; std::map buildContentFileIndexMap(const ESM::ESMReader& reader) const; public: StateManager(const std::filesystem::path& saves, const std::vector& contentFiles); void requestQuit() override; bool hasQuitRequest() const override; void askLoadRecent() override; void requestNewGame() override { mNewGameRequest = true; } void requestLoad(const std::filesystem::path& filepath) override { mLoadRequest = filepath; } State getState() const override; void newGame(bool bypass = false) override; ///< Start a new game. /// /// \param bypass Skip new game mechanics. void endGame(); 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 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; /// Loads the last saved file /** Used for quickload **/ void quickLoad() override; 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::filesystem::path& filepath) override; ///< Load a saved game file belonging to the given character. Character* getCurrentCharacter() override; ///< @note May return null. CharacterIterator characterBegin() override; ///< Any call to SaveGame and getCurrentCharacter can invalidate the returned /// iterator. CharacterIterator characterEnd() override; void update(float duration); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/000077500000000000000000000000001503074453300203635ustar00rootroot00000000000000openmw-openmw-0.49.0/apps/openmw/mwworld/action.cpp000066400000000000000000000037021503074453300223460ustar00rootroot00000000000000#include "action.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" const MWWorld::Ptr& MWWorld::Action::getTarget() const { return mTarget; } 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() {} void MWWorld::Action::execute(const Ptr& actor, bool noSound) { if (!mSoundId.empty() && !noSound) { MWSound::PlayMode envType = MWSound::PlayMode::Normal; // Action sounds should not have a distortion in GUI mode // example: take an item or drink a potion underwater if (actor == MWMechanics::getPlayer() && MWBase::Environment::get().getWindowManager()->isGuiMode()) { envType = MWSound::PlayMode::NoEnv; } 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) MWBase::Environment::get().getSoundManager()->playSound3D( (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); } } executeImp(actor); } void MWWorld::Action::setSound(const ESM::RefId& id) { mSoundId = id; } void MWWorld::Action::setSoundOffset(float offset) { mSoundOffset = offset; } openmw-openmw-0.49.0/apps/openmw/mwworld/action.hpp000066400000000000000000000020601503074453300223470ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTION_H #define GAME_MWWORLD_ACTION_H #include #include #include "ptr.hpp" namespace MWWorld { /// \brief Abstract base for actions class Action { ESM::RefId mSoundId; bool mKeepSound; float mSoundOffset; Ptr mTarget; // not implemented Action(const Action& action); Action& operator=(const Action& action); virtual void executeImp(const Ptr& actor) = 0; protected: void setTarget(const Ptr&); public: const Ptr& getTarget() const; 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 ~Action(); 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 ESM::RefId& id); void setSoundOffset(float offset); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionalchemy.cpp000066400000000000000000000013171503074453300237110ustar00rootroot00000000000000#include "actionalchemy.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionAlchemy::ActionAlchemy(bool force) : Action(false) , mForce(force) { } void ActionAlchemy::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; if (!mForce && MWMechanics::isPlayerInCombat()) { // Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage3}"); return; } MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Alchemy); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionalchemy.hpp000066400000000000000000000004661503074453300237220ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONALCHEMY_H #define GAME_MWWORLD_ACTIONALCHEMY_H #include "action.hpp" namespace MWWorld { class ActionAlchemy : public Action { bool mForce; void executeImp(const Ptr& actor) override; public: ActionAlchemy(bool force = false); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionapply.cpp000066400000000000000000000005021503074453300234070ustar00rootroot00000000000000#include "actionapply.hpp" #include "class.hpp" namespace MWWorld { ActionApply::ActionApply(const Ptr& object, const ESM::RefId& id) : Action(false, object) , mId(id) { } void ActionApply::executeImp(const Ptr& actor) { actor.getClass().consume(getTarget(), actor); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionapply.hpp000066400000000000000000000005751503074453300234260ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONAPPLY_H #define GAME_MWWORLD_ACTIONAPPLY_H #include "action.hpp" #include #include namespace MWWorld { class ActionApply : public Action { ESM::RefId mId; void executeImp(const Ptr& actor) override; public: ActionApply(const Ptr& object, const ESM::RefId& id); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actiondoor.cpp000066400000000000000000000005531503074453300232330ustar00rootroot00000000000000#include "actiondoor.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWWorld { ActionDoor::ActionDoor(const MWWorld::Ptr& object) : Action(false, object) { } void ActionDoor::executeImp(const MWWorld::Ptr& actor) { MWBase::Environment::get().getWorld()->activateDoor(getTarget()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actiondoor.hpp000066400000000000000000000004601503074453300232350ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONDOOR_H #define GAME_MWWORLD_ACTIONDOOR_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class ActionDoor : public Action { void executeImp(const MWWorld::Ptr& actor) override; public: ActionDoor(const Ptr& object); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actioneat.cpp000066400000000000000000000010071503074453300230340ustar00rootroot00000000000000#include "actioneat.hpp" #include #include "../mwmechanics/actorutil.hpp" #include "class.hpp" namespace MWWorld { void ActionEat::executeImp(const Ptr& actor) { 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) { } } openmw-openmw-0.49.0/apps/openmw/mwworld/actioneat.hpp000066400000000000000000000004311503074453300230410ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONEAT_H #define GAME_MWWORLD_ACTIONEAT_H #include "action.hpp" namespace MWWorld { class ActionEat : public Action { void executeImp(const Ptr& actor) override; public: ActionEat(const MWWorld::Ptr& object); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionequip.cpp000066400000000000000000000067761503074453300234300ustar00rootroot00000000000000#include "actionequip.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "class.hpp" #include "inventorystore.hpp" namespace MWWorld { ActionEquip::ActionEquip(const MWWorld::Ptr& object, bool force) : Action(false, object) , mForce(force) { } void ActionEquip::executeImp(const Ptr& actor) { MWWorld::Ptr object = getTarget(); MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); if (object.getClass().hasItemHealth(object) && object.getCellRef().getCharge() == 0) { if (actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); return; } if (!mForce) { 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) { case 0: return; default: break; } } // slots that this item can be equipped in std::pair, bool> slots_ = getTarget().getClass().getEquipmentSlots(getTarget()); if (slots_.first.empty()) return; // retrieve ContainerStoreIterator to the item MWWorld::ContainerStoreIterator it = invStore.begin(); for (; it != invStore.end(); ++it) { if (*it == object) { break; } } if (it == invStore.end()) 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) { // if the item is equipped already, nothing to do if (invStore.getSlot(*slot) == it) return; if (invStore.getSlot(*slot) == invStore.end()) { // slot is not occupied invStore.equip(*slot, it); break; } } // all slots are occupied -> cycle // move all slots one towards begin(), then equip the item in the slot that is now free if (slot == slots_.first.end()) { ContainerStoreIterator enchItem = invStore.getSelectedEnchantItem(); bool reEquip = false; for (slot = slots_.first.begin(); slot != slots_.first.end(); ++slot) { invStore.unequipSlot(*slot, false); if (slot + 1 != slots_.first.end()) { invStore.equip(*slot, invStore.getSlot(*(slot + 1))); } else { invStore.equip(*slot, it); } // Fix for issue of selected enchated item getting remmoved on cycle if (invStore.getSlot(*slot) == enchItem) { reEquip = true; } } if (reEquip) { invStore.setSelectedEnchantItem(enchItem); } } } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionequip.hpp000066400000000000000000000005431503074453300234170ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONEQUIP_H #define GAME_MWWORLD_ACTIONEQUIP_H #include "action.hpp" namespace MWWorld { class ActionEquip : public Action { bool mForce; void executeImp(const Ptr& actor) override; public: /// @param item to equip ActionEquip(const Ptr& object, bool force = false); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionharvest.cpp000066400000000000000000000071321503074453300237440ustar00rootroot00000000000000#include "actionharvest.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "class.hpp" #include "containerstore.hpp" namespace MWWorld { ActionHarvest::ActionHarvest(const MWWorld::Ptr& container) : Action(true, container) { setSound(ESM::RefId::stringRefId("Item Ingredient Up")); } 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); store.resolve(); MWWorld::ContainerStore& actorStore = actor.getClass().getContainerStore(actor); std::map takenMap; for (MWWorld::ContainerStoreIterator it = store.begin(); it != store.end(); ++it) { if (!it->getClass().showsInInventory(*it)) continue; 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); 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) if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) { std::ostringstream stream; int lineCount = 0; const static int maxLines = 10; for (const auto& pair : takenMap) { const std::string& itemName = pair.first; int itemCount = pair.second; lineCount++; if (lineCount == maxLines) stream << "\n..."; 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 std::string msgBox; if (itemCount == 1) { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage60}"); msgBox = Misc::StringUtils::format(msgBox, itemName); } else { msgBox = MyGUI::LanguageManager::getInstance().replaceTags("\n#{sNotifyMessage61}"); msgBox = Misc::StringUtils::format(msgBox, itemCount, itemName); } stream << msgBox; } std::string tooltip = stream.str(); // remove the first newline (easier this way) if (tooltip.size() > 0 && tooltip[0] == '\n') tooltip.erase(0, 1); if (tooltip.size() > 0) MWBase::Environment::get().getWindowManager()->messageBox(tooltip); } // Update animation object MWBase::Environment::get().getWorld()->disable(target); MWBase::Environment::get().getWorld()->enable(target); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionharvest.hpp000066400000000000000000000006251503074453300237510ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONHARVEST_H #define GAME_MWWORLD_ACTIONHARVEST_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class ActionHarvest : public Action { void executeImp(const MWWorld::Ptr& actor) override; public: ActionHarvest(const Ptr& container); ///< \param container The Container the Player has activated. }; } #endif // ACTIONOPEN_H openmw-openmw-0.49.0/apps/openmw/mwworld/actionopen.cpp000066400000000000000000000015461503074453300232340ustar00rootroot00000000000000#include "actionopen.hpp" #include "../mwbase/environment.hpp" #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) { } void ActionOpen::executeImp(const MWWorld::Ptr& actor) { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return; if (actor != MWMechanics::getPlayer()) return; if (!MWBase::Environment::get().getMechanicsManager()->onOpen(getTarget())) return; MWMechanics::diseaseContact(actor, getTarget()); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, getTarget()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionopen.hpp000066400000000000000000000005661503074453300232420ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONOPEN_H #define GAME_MWWORLD_ACTIONOPEN_H #include "action.hpp" 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. }; } #endif // ACTIONOPEN_H openmw-openmw-0.49.0/apps/openmw/mwworld/actionread.cpp000066400000000000000000000037011503074453300232010ustar00rootroot00000000000000#include "actionread.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "class.hpp" #include "esmstore.hpp" namespace MWWorld { ActionRead::ActionRead(const MWWorld::Ptr& object) : Action(false, object) { } 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() == &player.getClass().getContainerStore(player)) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage4}"); return; } 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 = player.getClass().getNpcStats(player); // Skill gain from books ESM::RefId skill = ESM::Skill::indexToRefId(ref->mBase->mData.mSkillId); if (!skill.empty() && !npcStats.hasBeenUsed(ref->mBase->mId)) { MWBase::Environment::get().getLuaManager()->skillLevelUp(player, skill, "book"); npcStats.flagAsUsed(ref->mBase->mId); } } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionread.hpp000066400000000000000000000005271503074453300232110ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONREAD_H #define GAME_MWWORLD_ACTIONREAD_H #include "action.hpp" namespace MWWorld { class ActionRead : public Action { void executeImp(const MWWorld::Ptr& actor) override; public: /// @param book or scroll to read ActionRead(const Ptr& object); }; } #endif // ACTIONREAD_H openmw-openmw-0.49.0/apps/openmw/mwworld/actionrepair.cpp000066400000000000000000000013171503074453300235510ustar00rootroot00000000000000#include "actionrepair.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionRepair::ActionRepair(const Ptr& item, bool force) : Action(false, item) , mForce(force) { } void ActionRepair::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; if (!mForce && MWMechanics::isPlayerInCombat()) { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage2}"); return; } MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Repair, getTarget()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionrepair.hpp000066400000000000000000000005521503074453300235560ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONREPAIR_H #define GAME_MWWORLD_ACTIONREPAIR_H #include "action.hpp" namespace MWWorld { class ActionRepair : public Action { bool mForce; void executeImp(const Ptr& actor) override; public: /// @param item repair hammer ActionRepair(const Ptr& item, bool force = false); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionsoulgem.cpp000066400000000000000000000025561503074453300237500ustar00rootroot00000000000000#include "actionsoulgem.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) : Action(false, object) { } void ActionSoulgem::executeImp(const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; if (MWMechanics::isPlayerInCombat()) { // Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage5}"); return; } 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); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionsoulgem.hpp000066400000000000000000000005131503074453300237440ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONSOULGEM_H #define GAME_MWWORLD_ACTIONSOULGEM_H #include "action.hpp" namespace MWWorld { class ActionSoulgem : public Action { void executeImp(const MWWorld::Ptr& actor) override; public: /// @param soulgem to use ActionSoulgem(const Ptr& object); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actiontake.cpp000066400000000000000000000026311503074453300232130ustar00rootroot00000000000000#include "actiontake.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwgui/inventorywindow.hpp" #include "class.hpp" #include "containerstore.hpp" namespace MWWorld { ActionTake::ActionTake(const MWWorld::Ptr& object) : Action(true, object) { } void ActionTake::executeImp(const Ptr& actor) { // When in GUI mode, we should use drag and drop if (actor == MWBase::Environment::get().getWorld()->getPlayerPtr()) { MWGui::GuiMode mode = MWBase::Environment::get().getWindowManager()->getMode(); if (mode == MWGui::GM_Inventory || mode == MWGui::GM_Container) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->pickUpObject(getTarget()); return; } } 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); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actiontake.hpp000066400000000000000000000004351503074453300232200ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONTAKE_H #define GAME_MWWORLD_ACTIONTAKE_H #include "action.hpp" namespace MWWorld { class ActionTake : public Action { void executeImp(const Ptr& actor) override; public: ActionTake(const MWWorld::Ptr& object); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actiontalk.cpp000066400000000000000000000007261503074453300232250ustar00rootroot00000000000000#include "actiontalk.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { ActionTalk::ActionTalk(const Ptr& actor) : Action(false, actor) { } void ActionTalk::executeImp(const Ptr& actor) { if (actor == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Dialogue, getTarget()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actiontalk.hpp000066400000000000000000000005201503074453300232220ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONTALK_H #define GAME_MWWORLD_ACTIONTALK_H #include "action.hpp" namespace MWWorld { class ActionTalk : public Action { void executeImp(const Ptr& actor) override; public: ActionTalk(const Ptr& actor); ///< \param actor The actor the player is talking to }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actionteleport.cpp000066400000000000000000000074131503074453300241300ustar00rootroot00000000000000#include "actionteleport.hpp" #include #include #include "../mwbase/environment.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(ESM::RefId cellId, const ESM::Position& position, bool teleportFollowers) : Action(true) , mCellId(cellId) , mPosition(position) , mTeleportFollowers(teleportFollowers) { } void ActionTeleport::executeImp(const Ptr& actor) { if (mTeleportFollowers) { // Find any NPCs that are following the actor and teleport them with him std::set followers; 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); } teleport(actor); } void ActionTeleport::teleport(const Ptr& actor) { MWBase::World* world = MWBase::Environment::get().getWorld(); 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); world->changeToCell(mCellId, mPosition, true); teleported = world->getPlayerPtr(); } else { if (actor.getClass().getCreatureStats(actor).getAiSequence().isInCombat(world->getPlayerPtr())) { actor.getClass().getCreatureStats(actor).getAiSequence().stopCombat(); return; } else 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 toExterior, bool includeHostiles) { std::set followers; MWBase::Environment::get().getMechanicsManager()->getActorsFollowing(actor, followers); for (std::set::iterator it = followers.begin(); it != followers.end(); ++it) { MWWorld::Ptr follower = *it; const ESM::RefId& script = follower.getClass().getScript(follower); if (!includeHostiles && follower.getClass().getCreatureStats(follower).getAiSequence().isInCombat(actor)) continue; 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) continue; out.emplace(follower); } } } openmw-openmw-0.49.0/apps/openmw/mwworld/actionteleport.hpp000066400000000000000000000023471503074453300241360ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONTELEPORT_H #define GAME_MWWORLD_ACTIONTELEPORT_H #include #include #include #include #include "action.hpp" namespace MWWorld { class ActionTeleport : public Action { 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); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/actiontrap.cpp000066400000000000000000000026341503074453300232400ustar00rootroot00000000000000#include "actiontrap.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" namespace MWWorld { void ActionTrap::executeImp(const Ptr& actor) { osg::Vec3f actorPosition(actor.getRefData().getPosition().asVec3()); osg::Vec3f trapPosition(mTrapSource.getRefData().getPosition().asVec3()); float trapRange = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); // Note: can't just detonate the trap at the trapped object's location and use the blast // 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 { MWMechanics::CastSpell cast(mTrapSource, mTrapSource); cast.mHitPosition = trapPosition; cast.cast(mSpellId); } else // player activated object within range of trap, or NPC activated trap { MWMechanics::CastSpell cast(mTrapSource, actor); cast.mHitPosition = actorPosition; cast.cast(mSpellId); } mTrapSource.getCellRef().setTrap(ESM::RefId()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/actiontrap.hpp000066400000000000000000000011001503074453300232300ustar00rootroot00000000000000#ifndef GAME_MWWORLD_ACTIONTRAP_H #define GAME_MWWORLD_ACTIONTRAP_H #include #include "action.hpp" namespace MWWorld { class ActionTrap : public Action { 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 openmw-openmw-0.49.0/apps/openmw/mwworld/cell.cpp000066400000000000000000000066101503074453300220110ustar00rootroot00000000000000#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 = {}; } } openmw-openmw-0.49.0/apps/openmw/mwworld/cell.hpp000066400000000000000000000042071503074453300220160ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/cellpreloader.cpp000066400000000000000000000377061503074453300237210ustar00rootroot00000000000000#include "cellpreloader.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwrender/landmanager.hpp" #include "cellstore.hpp" #include "class.hpp" namespace MWWorld { namespace { 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); } } struct ListModelsVisitor { bool operator()(const MWWorld::ConstPtr& ptr) { ptr.getClass().getModelsToPreload(ptr, mOut); return true; } std::vector& mOut; }; /// Worker thread item: preload models in a cell. class PreloadItem : public SceneUtil::WorkItem { public: /// Constructor to be called from the main thread. 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()) , mCellLocation(cell->getCell()->getExteriorCellLocation()) , mCellId(cell->getCell()->getId()) , mSceneManager(sceneManager) , mBulletShapeManager(bulletShapeManager) , mKeyframeManager(keyframeManager) , mTerrain(terrain) , mLandManager(landManager) , mPreloadInstances(preloadInstances) , mAbort(false) { mTerrainView = mTerrain->createView(); ListModelsVisitor visitor{ mMeshes }; cell->forEachConst(visitor); } void abort() override { mAbort = true; } /// Preload work to be called from the worker thread. void doWork() override { if (mIsExterior) { try { mTerrain->cacheCell(mTerrainView.get(), mCellLocation.mX, mCellLocation.mY); mPreloadedObjects.insert(mLandManager->getLand(mCellLocation)); } catch (const std::exception& e) { Log(Debug::Warning) << "Failed to cache terrain for exterior cell " << mCellLocation << ": " << e.what(); } } VFS::Path::Normalized mesh; VFS::Path::Normalized kfname; for (std::string_view path : mMeshes) { if (mAbort) break; try { const VFS::Manager& vfs = *mSceneManager->getVFS(); mesh = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(path)); mesh = Misc::ResourceHelpers::correctActorModelPath(mesh, &vfs); if (!vfs.exists(mesh)) continue; if (Misc::getFileName(mesh).starts_with('x') && Misc::getFileExtension(mesh) == "nif") { kfname = mesh; kfname.changeExtension("kf"); if (vfs.exists(kfname)) mPreloadedObjects.insert(mKeyframeManager->get(kfname)); } mPreloadedObjects.insert(mSceneManager->getTemplate(mesh)); if (mPreloadInstances) mPreloadedObjects.insert(mBulletShapeManager->cacheInstance(mesh)); else mPreloadedObjects.insert(mBulletShapeManager->getShape(mesh)); } catch (const std::exception& e) { Log(Debug::Warning) << "Failed to preload mesh \"" << path << "\" from cell " << mCellId << ": " << e.what(); } } } private: bool mIsExterior; ESM::ExteriorCellLocation mCellLocation; ESM::RefId mCellId; std::vector mMeshes; Resource::SceneManager* mSceneManager; Resource::BulletShapeManager* mBulletShapeManager; Resource::KeyframeManager* mKeyframeManager; Terrain::World* mTerrain; MWRender::LandManager* mLandManager; bool mPreloadInstances; std::atomic mAbort; 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; }; class TerrainPreloadItem : public SceneUtil::WorkItem { public: explicit TerrainPreloadItem(const std::vector>& views, Terrain::World* world, std::span preloadPositions) : mAbort(false) , mTerrainViews(views) , mWorld(world) , mPreloadPositions(preloadPositions.begin(), preloadPositions.end()) { } void doWork() override { for (unsigned int i = 0; i < mTerrainViews.size() && i < mPreloadPositions.size() && !mAbort; ++i) { mTerrainViews[i]->reset(); mWorld->preload(mTerrainViews[i], mPreloadPositions[i].mPosition, mPreloadPositions[i].mCellBounds, mAbort, mLoadingReporter); } mLoadingReporter.complete(); } void abort() override { mAbort = true; } void wait(Loading::Listener& listener) const { mLoadingReporter.wait(listener); } private: std::atomic mAbort; std::vector> mTerrainViews; Terrain::World* mWorld; std::vector mPreloadPositions; Loading::Reporter mLoadingReporter; }; /// Worker thread item: update the resource system's cache, effectively deleting unused entries. class UpdateCacheItem : public SceneUtil::WorkItem { public: UpdateCacheItem(Resource::ResourceSystem* resourceSystem, double referenceTime) : mReferenceTime(referenceTime) , mResourceSystem(resourceSystem) { } 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) : mResourceSystem(resourceSystem) , mBulletShapeManager(bulletShapeManager) , mTerrain(terrain) , mLandManager(landManager) , mExpiryDelay(0.0) , mPreloadInstances(true) , mLastResourceCacheUpdate(0.0) , mLoadedTerrainTimestamp(0.0) { } CellPreloader::~CellPreloader() { clearAllTasks(); } 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) { Log(Debug::Error) << "Error: can't preload objects for unloaded cell"; return; } PreloadMap::iterator found = mPreloadCells.find(&cell); if (found != mPreloadCells.end()) { // already preloaded, nothing to do other than updating the timestamp found->second.mTimeStamp = timestamp; return; } while (mPreloadCells.size() >= mMaxCacheSize) { // throw out oldest cell to make room PreloadMap::iterator oldestCell = mPreloadCells.begin(); double oldestTimestamp = std::numeric_limits::max(); double threshold = 1.0; // seconds for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end(); ++it) { if (it->second.mTimeStamp < oldestTimestamp) { oldestTimestamp = it->second.mTimeStamp; oldestCell = it; } } if (oldestTimestamp + threshold < timestamp) { 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)); mWorkQueue->addWorkItem(item); mPreloadCells.emplace(&cell, PreloadEntry(timestamp, item)); ++mAdded; } void CellPreloader::notifyLoaded(CellStore* cell) { PreloadMap::iterator found = mPreloadCells.find(cell); if (found != mPreloadCells.end()) { if (found->second.mWorkItem) { found->second.mWorkItem->abort(); found->second.mWorkItem = nullptr; } mPreloadCells.erase(found); ++mLoaded; } } void CellPreloader::clear() { for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();) { if (it->second.mWorkItem) { it->second.mWorkItem->abort(); it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); } } void CellPreloader::updateCache(double timestamp) { for (PreloadMap::iterator it = mPreloadCells.begin(); it != mPreloadCells.end();) { if (mPreloadCells.size() >= mMinCacheSize && it->second.mTimeStamp < timestamp - mExpiryDelay) { if (it->second.mWorkItem) { it->second.mWorkItem->abort(); it->second.mWorkItem = nullptr; } mPreloadCells.erase(it++); ++mExpired; } else ++it; } 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 mUpdateCacheItem = new UpdateCacheItem(mResourceSystem, timestamp); mWorkQueue->addWorkItem(mUpdateCacheItem, true); mLastResourceCacheUpdate = timestamp; } if (mTerrainPreloadItem && mTerrainPreloadItem->isDone()) { mLoadedTerrainPositions = mTerrainPreloadPositions; mLoadedTerrainTimestamp = timestamp; } } void CellPreloader::setExpiryDelay(double expiryDelay) { mExpiryDelay = expiryDelay; } void CellPreloader::setPreloadInstances(bool preload) { mPreloadInstances = preload; } void CellPreloader::setWorkQueue(osg::ref_ptr workQueue) { mWorkQueue = workQueue; } void CellPreloader::syncTerrainLoad(Loading::Listener& listener) { if (mTerrainPreloadItem != nullptr && !mTerrainPreloadItem->isDone()) mTerrainPreloadItem->wait(listener); } void CellPreloader::abortTerrainPreloadExcept(const PositionCellGrid* exceptPos) { if (exceptPos != nullptr && contains(mTerrainPreloadPositions, *exceptPos, Constants::CellSizeInUnits)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) { mTerrainPreloadItem->abort(); mTerrainPreloadItem->waitTillDone(); } setTerrainPreloadPositions({}); } void CellPreloader::setTerrainPreloadPositions(std::span positions) { if (positions.empty()) { mTerrainPreloadPositions.clear(); mLoadedTerrainPositions.clear(); } else if (contains(mTerrainPreloadPositions, positions, 128.f)) return; if (mTerrainPreloadItem && !mTerrainPreloadItem->isDone()) return; else { if (mTerrainViews.size() > positions.size()) mTerrainViews.resize(positions.size()); else if (mTerrainViews.size() < positions.size()) { for (unsigned int i = mTerrainViews.size(); i < positions.size(); ++i) mTerrainViews.emplace_back(mTerrain->createView()); } mTerrainPreloadPositions.assign(positions.begin(), positions.end()); if (!positions.empty()) { mTerrainPreloadItem = new TerrainPreloadItem(mTerrainViews, mTerrain, positions); mWorkQueue->addWorkItem(mTerrainPreloadItem); } } } 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); } } openmw-openmw-0.49.0/apps/openmw/mwworld/cellpreloader.hpp000066400000000000000000000077671503074453300237320ustar00rootroot00000000000000#ifndef OPENMW_MWWORLD_CELLPRELOADER_H #define OPENMW_MWWORLD_CELLPRELOADER_H #include "positioncellgrid.hpp" #include #include #include #include namespace osg { class Stats; } namespace Resource { class ResourceSystem; class BulletShapeManager; } namespace Terrain { class World; class View; } namespace MWRender { class LandManager; } namespace Loading { class Listener; } namespace MWWorld { class CellStore; class TerrainPreloadItem; class CellPreloader { public: 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 notifyLoaded(MWWorld::CellStore* cell); void clear(); /// Removes preloaded cells that have not had a preload request for a while. void updateCache(double timestamp); /// How long to keep a preloaded cell in cache after it's no longer requested. void setExpiryDelay(double expiryDelay); /// The minimum number of preloaded cells before unused cells get thrown out. void setMinCacheSize(std::size_t value) { mMinCacheSize = value; } /// The maximum number of preloaded cells. void setMaxCacheSize(std::size_t value) { mMaxCacheSize = value; } /// Enables the creation of instances in the preloading thread. void setPreloadInstances(bool preload); std::size_t getMaxCacheSize() const { return mMaxCacheSize; } std::size_t getCacheSize() const { return mPreloadCells.size(); } void setWorkQueue(osg::ref_ptr workQueue); void setTerrainPreloadPositions(std::span positions); void syncTerrainLoad(Loading::Listener& listener); void abortTerrainPreloadExcept(const PositionCellGrid* exceptPos); bool isTerrainLoaded(const PositionCellGrid& position, double referenceTime) const; void setTerrain(Terrain::World* terrain); 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; double mExpiryDelay; std::size_t mMinCacheSize = 0; std::size_t mMaxCacheSize = 0; bool mPreloadInstances; double mLastResourceCacheUpdate; struct PreloadEntry { PreloadEntry(double timestamp, osg::ref_ptr workItem) : mTimeStamp(timestamp) , mWorkItem(std::move(workItem)) { } PreloadEntry() : mTimeStamp(0.0) { } double mTimeStamp; osg::ref_ptr mWorkItem; }; typedef std::map PreloadMap; // Cells that are currently being preloaded, or have already finished preloading PreloadMap mPreloadCells; 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/cellref.cpp000066400000000000000000000336401503074453300225110ustar00rootroot00000000000000#include "cellref.hpp" #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 { CellRef::CellRef(const ESM::CellRef& ref) : mCellRef(ESM::ReferenceVariant(ref)) { } CellRef::CellRef(const ESM4::Reference& ref) : mCellRef(ESM::ReferenceVariant(ref)) { } CellRef::CellRef(const ESM4::ActorCharacter& ref) : mCellRef(ESM::ReferenceVariant(ref)) { } ESM::RefNum CellRef::getRefNum() const noexcept { 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); } ESM::RefNum CellRef::getOrAssignRefNum(ESM::RefNum& lastAssignedRefNum) { 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; } void CellRef::setRefNum(ESM::RefNum refNum) { 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 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); } ESM::RefId CellRef::getDestCell() const { 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 != getScale()) { mChanged = true; std::visit([scale](auto&& ref) { ref.mScale = scale; }, mCellRef.mVariant); } } void CellRef::setPosition(const ESM::Position& position) { mChanged = true; std::visit([&position](auto&& ref) { ref.mPos = position; }, mCellRef.mVariant); } float CellRef::getEnchantmentCharge() const { 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(const ESM::Enchantment& enchantment) const { const int maxCharge = MWMechanics::getEnchantmentCharge(enchantment); if (maxCharge == 0) { return 0; } else if (getEnchantmentCharge() == -1) { return 1; } else { return getEnchantmentCharge() / static_cast(maxCharge); } } void CellRef::setEnchantmentCharge(float charge) { if (charge != getEnchantmentCharge()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mEnchantmentCharge = charge; }, }, mCellRef.mVariant); } } void CellRef::setCharge(int charge) { std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mChargeInt = charge; }, }, mCellRef.mVariant); } void CellRef::applyChargeRemainderToBeSubtracted(float chargeRemainder) { auto esm3Visit = [&](ESM::CellRef& cellRef3) { cellRef3.mChargeIntRemainder -= std::abs(chargeRemainder); if (cellRef3.mChargeIntRemainder <= -1.0f) { float newChargeRemainder = std::modf(cellRef3.mChargeIntRemainder, &cellRef3.mChargeIntRemainder); cellRef3.mChargeInt += static_cast(cellRef3.mChargeIntRemainder); cellRef3.mChargeIntRemainder = newChargeRemainder; if (cellRef3.mChargeInt < 0) cellRef3.mChargeInt = 0; } }; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, esm3Visit, }, mCellRef.mVariant); } void CellRef::setChargeIntRemainder(float chargeRemainder) { std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mChargeIntRemainder = chargeRemainder; }, }, mCellRef.mVariant); } void CellRef::setChargeFloat(float charge) { std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mChargeFloat = charge; }, }, mCellRef.mVariant); } const std::string& CellRef::getGlobalVariable() const { 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 (!getGlobalVariable().empty()) { mChanged = true; 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 != getFactionRank()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::ActorCharacter&) {}, [&](auto&& ref) { ref.mFactionRank = factionRank; } }, mCellRef.mVariant); } } void CellRef::setOwner(const ESM::RefId& owner) { if (owner != getOwner()) { std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mOwner = owner; }, }, mCellRef.mVariant); } } void CellRef::setSoul(const ESM::RefId& soul) { if (soul != getSoul()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mSoul = soul; }, }, mCellRef.mVariant); } } void CellRef::setFaction(const ESM::RefId& faction) { if (faction != getFaction()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mFaction = faction; }, }, mCellRef.mVariant); } } void CellRef::setLockLevel(int lockLevel) { if (lockLevel != getLockLevel()) { mChanged = true; 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) { setLockLevel(lockLevel); setLocked(true); } void CellRef::unlock() { setLockLevel(-getLockLevel()); setLocked(false); } bool CellRef::isLocked() const { 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); } void CellRef::setLocked(bool locked) { std::visit(ESM::VisitOverload{ [&](ESM4::Reference& ref) { ref.mIsLocked = locked; }, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mIsLocked = locked; }, }, mCellRef.mVariant); } void CellRef::setTrap(const ESM::RefId& trap) { if (trap != getTrap()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mTrap = trap; }, }, mCellRef.mVariant); } } void CellRef::setKey(const ESM::RefId& key) { if (key != getKey()) { mChanged = true; std::visit(ESM::VisitOverload{ [&](ESM4::Reference& /*ref*/) {}, [&](ESM4::ActorCharacter&) {}, [&](ESM::CellRef& ref) { ref.mKey = key; }, }, mCellRef.mVariant); } } void CellRef::setCount(int value) { 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); } } void CellRef::writeState(ESM::ObjectState& state) const { std::visit(ESM::VisitOverload{ [&](const ESM4::Reference& /*ref*/) {}, [&](const ESM4::ActorCharacter&) {}, [&](const ESM::CellRef& ref) { state.mRef = ref; }, }, mCellRef.mVariant); } } openmw-openmw-0.49.0/apps/openmw/mwworld/cellref.hpp000066400000000000000000000244761503074453300225250ustar00rootroot00000000000000#ifndef OPENMW_MWWORLD_CELLREF_H #define OPENMW_MWWORLD_CELLREF_H #include #include #include #include namespace ESM { struct Enchantment; struct ObjectState; } namespace MWWorld { /// \brief Encapsulated variant of ESM::CellRef with change tracking class CellRef { protected: public: explicit CellRef(const ESM::CellRef& ref); explicit CellRef(const ESM4::Reference& ref); explicit CellRef(const ESM4::ActorCharacter& ref); // Note: Currently unused for items in containers 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() { setRefNum({}); } /// Does the RefNum have a content file? bool hasContentFile() const { return getRefNum().hasContentFile(); } // Id of object being referenced 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 { 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) ESM::RefId getDestCell() const; // Scale applied to mesh 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. 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(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 { 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 // Stores fractional part of mChargeInt void setChargeIntRemainder(float chargeRemainder); // The NPC that owns this object (and will get angry if you steal it) 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. const std::string& getGlobalVariable() const; void resetGlobalVariable(); // ID of creature trapped in this soul gem 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) 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 { 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 { 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(); 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); 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; // Has this CellRef changed since it was originally loaded? bool hasChanged() const { return mChanged; } private: bool mChanged = false; ESM::ReferenceVariant mCellRef; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/cellreflist.hpp000066400000000000000000000031621503074453300234060ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CELLREFLIST_H #define GAME_MWWORLD_CELLREFLIST_H #include #include "livecellref.hpp" namespace MWWorld { struct CellRefListBase { }; /// \brief Collection of references of one type template struct CellRefList : public CellRefListBase { typedef LiveCellRef LiveRef; typedef std::list List; List mList; /// Search for the given reference in the given reclist from /// ESMStore. Insert the reference into the list if a match is /// found. If not, throw an exception. /// Moved to cpp file, as we require a custom compare operator for it, /// 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(const ESM4::Reference& ref, const MWWorld::ESMStore& esmStore); void load(const ESM4::ActorCharacter& ref, const MWWorld::ESMStore& esmStore); LiveRef& insert(const LiveRef& item) { mList.push_back(item); return mList.back(); } /// Remove all references with the given refNum from this list. void remove(ESM::RefNum refNum) { for (typename List::iterator it = mList.begin(); it != mList.end();) { if (*it == refNum) mList.erase(it++); else ++it; } } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/cellstore.cpp000066400000000000000000001360521503074453300230720ustar00rootroot00000000000000#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 "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/recharge.hpp" #include "../mwmechanics/spellutil.hpp" #include "class.hpp" #include "containerstore.hpp" #include "esmstore.hpp" #include "inventorystore.hpp" #include "ptr.hpp" #include "worldmodel.hpp" namespace { 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 (auto iter(containerList.mList.begin()); iter != containerList.mList.end(); ++iter) { MWWorld::Ptr container(&*iter, nullptr); if (container.getRefData().getCustomData() == nullptr) continue; MWWorld::Ptr ptr = container.getClass().getContainerStore(container).search(id); if (!ptr.isEmpty()) return ptr; } return MWWorld::Ptr(); } 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) { MWWorld::Ptr actor(&*iter, cell); if (toIgnore.find(&*iter) != toIgnore.end()) continue; 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) { // references for (const MWWorld::LiveCellRef& liveCellRef : collection.mList) { if (ESM::isESM4Rec(T::sRecordId)) { // 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); state.save(writer); } } template void fixRestockingImpl(const T* base, RecordType& state) { // Workaround for old saves not containing negative quantities for (const auto& baseItem : base->mInventory.mList) { if (baseItem.mCount < 0) { for (auto& item : state.mInventory.mItems) { if (item.mRef.mCount > 0 && baseItem.mItem == item.mRef.mRefID) item.mRef.mCount = -item.mRef.mCount; } } } } template void fixRestocking(const T* base, RecordType& state) { } template <> void fixRestocking<>(const ESM::Creature* base, ESM::CreatureState& state) { fixRestockingImpl(base, state); } template <> void fixRestocking<>(const ESM::NPC* base, ESM::NpcState& state) { fixRestockingImpl(base, state); } 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 MWWorld::ESMStore& esmStore, MWWorld::CellStore* cellstore) { 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 (!reader.applyContentFileMapping(state.mRef.mRefNum)) return; // content file has been removed -> skip if (!MWWorld::LiveCellRef::checkState(state)) return; // not valid anymore with current content files -> skip const T* record = esmStore.get().search(state.mRef.mRefID); if (!record) return; 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.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(); 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.asVec3()); if (!iter->mData.isEnabled()) { iter->mData.enable(); MWBase::Environment::get().getWorld()->disable(ptr); } MWBase::Environment::get().getWorldModel()->registerPtr(ptr); 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(ESM::makeBlankCellRef(), record); ref.load(state); collection.mList.push_back(std::move(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, bool includeDeleted) { for (auto& v : list.mList) { if (!includeDeleted && !MWWorld::CellStore::isAccessible(v.mData, v.mRef)) continue; if (!visitor(MWWorld::Ptr(&v, &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 includeDeleted) { bool returnValue = true; Misc::tupleForEach(cellStore.mCellStoreImp->mRefLists, [&](auto& store) { returnValue = returnValue && forEachImp(visitor, store, cellStore, includeDeleted); }); return returnValue; } }; template void CellRefList::load(ESM::CellRef& ref, bool deleted, const MWWorld::ESMStore& esmStore) { const MWWorld::Store& store = esmStore.get(); if (const X* ptr = store.search(ref.mRefID)) { typename std::list::iterator iter = std::find(mList.begin(), mList.end(), ref.mRefNum); LiveRef liveCellRef(ref, ptr); if (deleted) liveCellRef.mData.setDeletedByContentFile(true); if (iter != mList.end()) *iter = std::move(liveCellRef); else mList.push_back(std::move(liveCellRef)); } else { Log(Debug::Warning) << "Warning: could not resolve cell reference " << ref.mRefID << " (dropping reference)"; } } 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(std::move(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) { MovedRefTracker::iterator found = mMovedToAnotherCell.find(ref); if (found != mMovedToAnotherCell.end()) return Ptr(ref, found->second); return Ptr(ref, this); } void CellStore::moveFrom(const Ptr& object, CellStore* from) { if (mState != State_Loaded) load(); mHasState = true; MovedRefTracker::iterator found = mMovedToAnotherCell.find(object.getBase()); if (found != mMovedToAnotherCell.end()) { // A cell we had previously moved an object to is returning it to us. assert(found->second == from); mMovedToAnotherCell.erase(found); } else { mMovedHere.insert(std::make_pair(object.getBase(), from)); } requestMergedRefsUpdate(); } 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?)"); MWBase::Environment::get().getWorldModel()->registerPtr(MWWorld::Ptr(object.getBase(), cellToMoveTo)); MovedRefTracker::iterator found = mMovedHere.find(object.getBase()); if (found != mMovedHere.end()) { // Special case - object didn't originate in this cell // Move it back to its original cell first CellStore* originalCell = found->second; assert(originalCell != this); originalCell->moveFrom(object, this); mMovedHere.erase(found); // Now that object is back to its rightful owner, we can move it if (cellToMoveTo != originalCell) { originalCell->moveTo(object, cellToMoveTo); } requestMergedRefsUpdate(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } cellToMoveTo->moveFrom(object, this); mMovedToAnotherCell.insert(std::make_pair(object.getBase(), cellToMoveTo)); requestMergedRefsUpdate(); return MWWorld::Ptr(object.getBase(), cellToMoveTo); } struct MergeVisitor { MergeVisitor(std::vector& mergeTo, const std::map& movedHere, const std::map& movedToAnotherCell) : mMergeTo(mergeTo) , mMovedHere(movedHere) , mMovedToAnotherCell(movedToAnotherCell) { } bool operator()(const MWWorld::Ptr& ptr) { if (mMovedToAnotherCell.find(ptr.getBase()) != mMovedToAnotherCell.end()) return true; mMergeTo.push_back(ptr.getBase()); return true; } void merge() { for (const auto& [base, _] : mMovedHere) mMergeTo.push_back(base); } private: std::vector& mMergeTo; const std::map& mMovedHere; const std::map& mMovedToAnotherCell; }; void CellStore::requestMergedRefsUpdate() { mRechargingItemsUpToDate = false; mMergedRefsNeedsUpdate = true; } void CellStore::updateMergedRefs(bool includeDeleted) const { mMergedRefs.clear(); MergeVisitor visitor(mMergedRefs, mMovedHere, mMovedToAnotherCell); CellStoreImp::forEachInternal(visitor, const_cast(*this), includeDeleted); visitor.merge(); mMergedRefsNeedsUpdate = false; } bool CellStore::movedHere(const MWWorld::Ptr& ptr) const { if (ptr.isEmpty()) return false; if (mMovedHere.find(ptr.getBase()) != mMovedHere.end()) return true; return 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) { std::apply([this](auto&... x) { (CellStoreImp::assignStoreToIndex(*this, x), ...); }, mCellStoreImp->mRefLists); mWaterLevel = mCellVariant.getWaterHeight(); } CellStore::~CellStore() = default; const MWWorld::Cell* CellStore::getCell() const { return &mCellVariant; } CellStore::State CellStore::getState() const { return mState; } const std::vector& CellStore::getPreloadedIds() const { return mIds; } bool CellStore::hasState() const { return mHasState; } bool CellStore::hasId(const ESM::RefId& id) const { if (mState == State_Unloaded) return false; if (mState == State_Preloaded) return std::binary_search(mIds.begin(), mIds.end(), id); return searchConst(id).isEmpty(); } template struct SearchVisitor { PtrType mFound; const ESM::RefId& mIdToFind; SearchVisitor(const ESM::RefId& id) : mIdToFind(id) { } bool operator()(const PtrType& ptr) { if (ptr.getCellRef().getRefId() == mIdToFind) { mFound = ptr; return false; } return true; } }; Ptr CellStore::search(const ESM::RefId& id) { SearchVisitor searchVisitor(id); forEach(searchVisitor); return searchVisitor.mFound; } ConstPtr CellStore::searchConst(const ESM::RefId& id) const { SearchVisitor searchVisitor(id); forEachConst(searchVisitor); return searchVisitor.mFound; } Ptr CellStore::searchViaActorId(int id) { if (Ptr ptr = ::searchViaActorId(get(), id, this, mMovedToAnotherCell); !ptr.isEmpty()) return ptr; if (Ptr ptr = ::searchViaActorId(get(), id, this, mMovedToAnotherCell); !ptr.isEmpty()) return ptr; for (const auto& [base, _] : mMovedHere) { MWWorld::Ptr actor(base, this); if (!actor.getClass().isActor()) continue; if (actor.getClass().getCreatureStats(actor).matchesActorId(id) && actor.getCellRef().getCount() > 0) return actor; } return Ptr(); } class RefNumSearchVisitor { ESM::RefNum mRefNum; public: RefNumSearchVisitor(ESM::RefNum refNum) : mRefNum(refNum) { } Ptr mFound; bool operator()(const Ptr& ptr) { if (ptr.getCellRef().getRefNum() == mRefNum) { mFound = ptr; return false; } return true; } }; float CellStore::getWaterLevel() const { if (isExterior()) return getCell()->getWaterHeight(); return mWaterLevel; } void CellStore::setWaterLevel(float level) { mWaterLevel = level; mHasState = true; } std::size_t CellStore::count() const { if (mMergedRefsNeedsUpdate) updateMergedRefs(); return mMergedRefs.size(); } void CellStore::load() { if (mState != State_Loaded) { if (mState == State_Preloaded) mIds.clear(); loadRefs(); mState = State_Loaded; } } void CellStore::preload() { if (mState == State_Unloaded) { listRefs(); mState = State_Preloaded; } } void CellStore::listRefs(const ESM::Cell& cell) { 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 < cell.mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. 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; bool moved = false; while (ESM::Cell::getNextRef( *reader, ref, deleted, cMRef, moved, ESM::Cell::GetNextRefMode::LoadOnlyNotMoved)) { if (deleted || moved) continue; // Don't list reference if it was moved to a different cell. ESM::MovedCellRefTracker::const_iterator iter = std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum); if (iter != cell.mMovedRefs.end()) { continue; } 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(); } } // List moved references, from separately tracked list. for (const auto& [ref, deleted] : cell.mLeasedRefs) { if (!deleted) mIds.push_back(ref.mRefID); } } 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); } template static void visitCell4ActorReferences( const ESM4::Cell& cell, const ESMStore& esmStore, ESM::ReadersCache& readers, ReferenceInvocable&& invocable) { for (const ESM4::ActorCharacter* ref : esmStore.get().getByCell(cell.mId)) invocable(*ref); for (const ESM4::ActorCharacter* ref : esmStore.get().getByCell(cell.mId)) invocable(*ref); } 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); }); } void CellStore::listRefs() { ESM::visit([&](auto&& cell) { listRefs(cell); }, mCellVariant); std::sort(mIds.begin(), mIds.end()); } 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 < cell.mContextList.size(); i++) { try { // Reopen the ESM reader and seek to the right position. 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; 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(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum); if (iter != cell.mMovedRefs.end()) { continue; } loadRef(ref, deleted, refNumToID); } } catch (std::exception& e) { 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 : cell.mLeasedRefs) { ESM::CellRef& ref = const_cast(leasedRef.first); bool deleted = leasedRef.second; loadRef(ref, deleted, refNumToID); } } 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 mCellVariant.isExterior(); } bool CellStore::isQuasiExterior() const { return mCellVariant.isQuasiExterior(); } Ptr CellStore::searchInContainer(const ESM::RefId& id) { bool oldState = mHasState; mHasState = true; if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; if (Ptr ptr = searchInContainerList(get(), id); !ptr.isEmpty()) return ptr; mHasState = oldState; return Ptr(); } 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) { 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; 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 ESM::RecNameInts foundType = static_cast(store.find(it->second)); if (foundType != 0) { Misc::tupleForEach(this->mCellStoreImp->mRefLists, [&ref, foundType](auto& x) { recNameSwitcher(x, foundType, [&ref](auto& storeIn) { storeIn.remove(ref.mRefNum); }); }); } } } ESM::RecNameInts foundType = static_cast(store.find(ref.mRefID)); bool handledType = false; if (foundType != 0) { 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; } 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) { mHasState = true; if (!mCellVariant.isExterior() && mCellVariant.hasWater()) mWaterLevel = state.mWaterLevel; mLastRespawn = MWWorld::TimeStamp(state.mLastRespawn); } void CellStore::saveState(ESM::CellState& state) const { state.mId = mCellVariant.getId(); 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 { if (mFogState.get()) { mFogState->save(writer, !mCellVariant.isExterior()); } } void CellStore::readFog(ESM::ESMReader& reader) { mFogState = std::make_unique(); mFogState->load(reader); } 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(); if (base->isDeleted() && !refNum.hasContentFile()) continue; // filtered out in writeReferenceCollection ESM::RefId movedTo = store->getCell()->getId(); writer.writeFormId(refNum, true, "MVRF"); writer.writeCellId(movedTo); } } void CellStore::readReferences(ESM::ESMReader& reader, GetCellStoreCallback* callback) { mHasState = true; while (reader.isNextSub("OBJE")) { unsigned int 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 = mStore.find(cref.mRefID); if (type == 0) { 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; } if (type != 0) { 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"); } } while (reader.isNextSub("MVRF")) { reader.cacheSubName(); ESM::RefNum refnum = reader.getFormId(true, "MVRF"); ESM::RefId movedToId = reader.getCellId(); if (!reader.applyContentFileMapping(refnum)) { 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 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)"; continue; } CellStore* otherCell = callback->getCellStore(movedToId); if (otherCell == nullptr) { Log(Debug::Warning) << "Warning: Dropping moved ref tag for " << movedRef.getCellRef().getRefId() << " (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; } if (otherCell == this) { // Should never happen unless someone's tampering with files. Log(Debug::Warning) << "Found invalid moved ref, ignoring"; continue; } moveTo(movedRef, otherCell); } requestMergedRefsUpdate(); } void CellStore::setFog(std::unique_ptr&& fog) { mFogState = std::move(fog); } ESM::FogState* CellStore::getFog() const { return mFogState.get(); } static void clearCorpse(const MWWorld::Ptr& ptr, const ESMStore& store) { const MWMechanics::CreatureStats& creatureStats = ptr.getClass().getCreatureStats(ptr); 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); } } void CellStore::rest(double hours) { if (mState == State_Loaded) { for (MWWorld::LiveCellRef& creature : get().mList) { Ptr ptr = getCurrentPtr(&creature); if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } for (MWWorld::LiveCellRef& npc : get().mList) { Ptr ptr = getCurrentPtr(&npc); if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { MWBase::Environment::get().getMechanicsManager()->restoreDynamicStats(ptr, hours, true); } } } } void CellStore::recharge(float duration) { if (duration <= 0) return; if (mState == State_Loaded) { for (MWWorld::LiveCellRef& creature : get().mList) { Ptr ptr = getCurrentPtr(&creature); if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } for (MWWorld::LiveCellRef& npc : get().mList) { Ptr ptr = getCurrentPtr(&npc); if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { ptr.getClass().getContainerStore(ptr).rechargeItems(duration); } } for (MWWorld::LiveCellRef& container : get().mList) { 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); } } rechargeItems(duration); } } void CellStore::respawn() { if (mState == State_Loaded) { 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(get().mList.begin()); it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); ptr.getClass().respawn(ptr); } } for (CellRefList::List::iterator it(get().mList.begin()); it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); clearCorpse(ptr, mStore); ptr.getClass().respawn(ptr); } for (CellRefList::List::iterator it(get().mList.begin()); it != get().mList.end(); ++it) { Ptr ptr = getCurrentPtr(&*it); 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; }); } } void MWWorld::CellStore::rechargeItems(float duration) { if (!mRechargingItemsUpToDate) { updateRechargingItems(); mRechargingItemsUpToDate = true; } for (const auto& [item, charge] : mRechargingItems) { MWMechanics::rechargeItem(item, charge, duration); } } void MWWorld::CellStore::updateRechargingItems() { mRechargingItems.clear(); const auto update = [this](auto& list) { for (auto& item : list) { Ptr ptr = getCurrentPtr(&item); if (!ptr.isEmpty() && ptr.getCellRef().getCount() > 0) { checkItem(ptr); } } }; update(get().mList); update(get().mList); update(get().mList); update(get().mList); } void MWWorld::CellStore::checkItem(const Ptr& ptr) { const ESM::RefId& enchantmentId = ptr.getClass().getEnchantment(ptr); if (enchantmentId.empty()) return; const ESM::Enchantment* enchantment = mStore.get().search(enchantmentId); if (!enchantment) { 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(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(); } } openmw-openmw-0.49.0/apps/openmw/mwworld/cellstore.hpp000066400000000000000000000371641503074453300231030ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CELLSTORE_H #define GAME_MWWORLD_CELLSTORE_H #include #include #include #include #include #include #include #include #include #include "cell.hpp" #include "cellreflist.hpp" #include "livecellref.hpp" #include #include #include #include "ptr.hpp" #include "timestamp.hpp" namespace ESM { class ReadersCache; struct Cell; struct CellState; 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: enum State { State_Unloaded, State_Preloaded, State_Loaded }; /// 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); } /// 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); void rest(double hours); void recharge(float duration); /// 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; } /// @param readerList The readers to use for loading of the cell on-demand. CellStore(MWWorld::Cell&& cell, const MWWorld::ESMStore& store, ESM::ReadersCache& readers); CellStore(const CellStore&) = delete; CellStore(CellStore&&) = delete; CellStore& operator=(const CellStore&) = delete; CellStore& operator=(CellStore&&) = delete; ~CellStore(); const MWWorld::Cell* getCell() const; State getState() const; const std::vector& getPreloadedIds() const; ///< Get Ids of objects in this cell, only valid in State_Preloaded bool hasState() const; ///< Does this cell have state that needs to be stored in a saved game file? 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. 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. 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 searchViaActorId(int id); ///< Will return an empty Ptr if cell is not loaded. float getWaterLevel() const; bool movedHere(const MWWorld::Ptr& ptr) const; void setWaterLevel(float level); void setFog(std::unique_ptr&& fog); ///< \note Takes ownership of the pointer ESM::FogState* getFog() const; std::size_t count() const; ///< Return total number of references, including deleted ones. void load(); ///< Load references 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, bool includeDeleted = false) { if (mState != State_Loaded) return false; if (mMergedRefsNeedsUpdate) updateMergedRefs(includeDeleted); if (mMergedRefs.empty()) return true; mHasState = true; for (LiveCellRefBase* mergedRef : mMergedRefs) { if (!includeDeleted && !isAccessible(mergedRef->mData, mergedRef->mRef)) continue; 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, bool includeDeleted = false) const { if (mState != State_Loaded) return false; if (mMergedRefsNeedsUpdate) updateMergedRefs(includeDeleted); for (const LiveCellRefBase* mergedRef : mMergedRefs) { if (!includeDeleted && !isAccessible(mergedRef->mData, mergedRef->mRef)) continue; 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, bool includeDeleted = false) { if (mState != State_Loaded) return false; if (mMergedRefsNeedsUpdate) updateMergedRefs(includeDeleted); if (mMergedRefs.empty()) return true; mHasState = true; for (LiveCellRefBase& base : get().mList) { if (mMovedToAnotherCell.contains(&base)) continue; if (!includeDeleted && !isAccessible(base.mData, base.mRef)) continue; 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; } // 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 isQuasiExterior() const; Ptr searchInContainer(const ESM::RefId& id); void loadState(const ESM::CellState& state); void saveState(ESM::CellState& state) const; void writeFog(ESM::ESMWriter& writer) const; void readFog(ESM::ESMReader& reader); void writeReferences(ESM::ESMWriter& writer) const; struct GetCellStoreCallback { ///@note must return nullptr if the cell is not found virtual CellStore* getCellStore(const ESM::RefId& cellId) = 0; virtual ~GetCellStoreCallback() = default; }; /// @param callback to use for retrieving of additional CellStore objects by ID (required for resolving moved /// references) void readReferences(ESM::ESMReader& reader, GetCellStoreCallback* callback); void respawn(); ///< Check mLastRespawn and respawn references if necessary. This is a no-op if the cell is not loaded. Ptr getMovedActor(int actorId) const; Ptr getPtr(ESM::RefId id); private: friend struct CellStoreImp; const MWWorld::ESMStore& mStore; ESM::ReadersCache& mReaders; // 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; MWWorld::Cell mCellVariant; State mState; bool mHasState; std::vector mIds; float mWaterLevel; MWWorld::TimeStamp mLastRespawn; template static constexpr std::size_t getTypeIndex() { static_assert(Misc::TupleHasType, CellStoreTuple>::value); return Misc::TupleTypeIndex, CellStoreTuple>::value; } std::unique_ptr mCellStoreImp; std::vector mCellRefLists; template CellRefList& get() { mHasState = true; return static_cast&>(*mCellRefLists[getTypeIndex()]); } template const CellRefList& get() const { return static_cast&>(*mCellRefLists[getTypeIndex()]); } 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 mutable std::vector mMergedRefs; mutable bool mMergedRefsNeedsUpdate = false; // 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 requestMergedRefsUpdate(); void updateMergedRefs(bool includeDeleted = false) const; // (item, max charge) typedef std::vector> TRechargingItems; TRechargingItems mRechargingItems; bool mRechargingItemsUpToDate; void updateRechargingItems(); void rechargeItems(float duration); void checkItem(const Ptr& ptr); /// 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. /// }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/cellvisitors.hpp000066400000000000000000000010101503074453300236060ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CELLVISITORS_H #define GAME_MWWORLD_CELLVISITORS_H #include #include #include "ptr.hpp" namespace MWWorld { struct ListAndResetObjectsVisitor { std::vector mObjects; bool operator()(const MWWorld::Ptr& ptr) { if (ptr.getRefData().getBaseNode()) { ptr.getRefData().setBaseNode(nullptr); } mObjects.push_back(ptr); return true; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/class.cpp000066400000000000000000000404361503074453300222030ustar00rootroot00000000000000#include "class.hpp" #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/esmstore.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/npcstats.hpp" namespace MWWorld { 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, 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::consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const { return false; } 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"); } bool Class::canSell(const MWWorld::ConstPtr& item, int npcServices) const { return false; } int Class::getServices(const ConstPtr& actor) const { throw std::runtime_error("class does not have services"); } MWMechanics::CreatureStats& Class::getCreatureStats(const Ptr& ptr) const { throw std::runtime_error("class does not have creature stats"); } MWMechanics::NpcStats& Class::getNpcStats(const Ptr& ptr) const { throw std::runtime_error("class does not have NPC stats"); } bool Class::hasItemHealth(const ConstPtr& ptr) const { return false; } int Class::getItemHealth(const ConstPtr& ptr) const { if (ptr.getCellRef().getCharge() == -1) return getItemMaxHealth(ptr); else return ptr.getCellRef().getCharge(); } float Class::getItemNormalizedHealth(const ConstPtr& ptr) const { if (getItemMaxHealth(ptr) == 0) { return 0.f; } else { return getItemHealth(ptr) / static_cast(getItemMaxHealth(ptr)); } } int Class::getItemMaxHealth(const ConstPtr& ptr) const { throw std::runtime_error("class does not have item health"); } bool Class::evaluateHit(const Ptr& ptr, Ptr& victim, osg::Vec3f& hitPosition) const { throw std::runtime_error("class cannot hit"); } 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 hit"); } void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const { throw std::runtime_error("class cannot be hit"); } std::unique_ptr Class::activate(const Ptr& ptr, const Ptr& actor) const { return std::make_unique(); } std::unique_ptr Class::use(const Ptr& ptr, bool force) const { return std::make_unique(); } ContainerStore& Class::getContainerStore(const Ptr& ptr) const { throw std::runtime_error("class does not have a container store"); } InventoryStore& Class::getInventoryStore(const Ptr& ptr) const { throw std::runtime_error("class does not have an inventory store"); } bool Class::hasInventoryStore(const ConstPtr& ptr) const { return false; } bool Class::canLock(const ConstPtr& ptr) const { return false; } void Class::setRemainingUsageTime(const Ptr& ptr, float duration) const { throw std::runtime_error("class does not support time-based uses"); } float Class::getRemainingUsageTime(const ConstPtr& ptr) const { return -1; } ESM::RefId Class::getScript(const ConstPtr& ptr) const { return ESM::RefId(); } float Class::getMaxSpeed(const Ptr& ptr) const { return 0; } float Class::getCurrentSpeed(const Ptr& ptr) const { return 0; } float Class::getJump(const Ptr& ptr) const { return 0; } int Class::getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not support enchanting"); } MWMechanics::Movement& Class::getMovementSettings(const Ptr& ptr) const { throw std::runtime_error("movement settings not supported by class"); } osg::Vec3f Class::getRotationVector(const Ptr& ptr) const { return osg::Vec3f(0, 0, 0); } std::pair, bool> Class::getEquipmentSlots(const ConstPtr& ptr) const { return std::make_pair(std::vector(), false); } ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const { return {}; } int Class::getValue(const ConstPtr& ptr) const { throw std::logic_error("value not supported by this class"); } float Class::getCapacity(const MWWorld::Ptr& ptr) const { throw std::runtime_error("capacity not supported by this class"); } float Class::getWeight(const ConstPtr& ptr) const { throw std::runtime_error("weight not supported by this class"); } float Class::getEncumbrance(const MWWorld::Ptr& ptr) const { throw std::runtime_error("encumbrance not supported by class"); } bool Class::isEssential(const MWWorld::ConstPtr& ptr) const { return false; } float Class::getArmorRating(const MWWorld::Ptr& ptr) const { throw std::runtime_error("Class does not support armor rating"); } const Class& Class::get(unsigned int key) { const auto& classes = getClasses(); auto iter = classes.find(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 { throw std::runtime_error("class does not support persistence"); } void Class::registerClass(Class& instance) { getClasses().emplace(instance.getType(), &instance); } const ESM::RefId& Class::getUpSoundId(const ConstPtr& ptr) const { throw std::runtime_error("class does not have an up sound"); } const ESM::RefId& Class::getDownSoundId(const ConstPtr& ptr) const { throw std::runtime_error("class does not have an down sound"); } ESM::RefId Class::getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const { throw std::runtime_error("class does not support soundgen look up"); } const std::string& Class::getInventoryIcon(const MWWorld::ConstPtr& ptr) const { throw std::runtime_error("class does not have any inventory icon"); } MWGui::ToolTipInfo Class::getToolTipInfo(const ConstPtr& ptr, int count) const { throw std::runtime_error("class does not have a tool tip"); } 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 // being shown or taken. return (ptr.getCellRef().getRefId() != "werewolfrobe"); } bool Class::hasToolTip(const ConstPtr& ptr) const { return true; } ESM::RefId Class::getEnchantment(const ConstPtr& ptr) const { return ESM::RefId(); } void Class::adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const {} std::string_view Class::getModel(const MWWorld::ConstPtr& ptr) const { return {}; } VFS::Path::Normalized Class::getCorrectedModel(const MWWorld::ConstPtr& ptr) const { std::string_view model = getModel(ptr); if (!model.empty()) return Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(model)); return {}; } bool Class::useAnim() const { return false; } void Class::getModelsToPreload(const ConstPtr& ptr, std::vector& models) const { std::string_view model = getModel(ptr); if (!model.empty()) models.push_back(model); } 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"); } std::pair Class::canBeEquipped(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& npc) const { return { 1, {} }; } void Class::adjustPosition(const MWWorld::Ptr& ptr, bool force) const {} std::unique_ptr Class::defaultItemActivate(const Ptr& ptr, const Ptr& actor) const { if (!MWBase::Environment::get().getWindowManager()->isAllowed(MWGui::GW_Inventory)) return std::make_unique(); if (actor.getClass().isNpc() && actor.getClass().getNpcStats(actor).isWerewolf()) { 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::unique_ptr action = std::make_unique("#{sWerewolfRefusal}"); if (sound) action->setSound(sound->mId); return action; } std::unique_ptr action = std::make_unique(ptr); action->setSound(getUpSoundId(ptr)); return action; } 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 { Ptr newPtr = copyToCellImpl(ptr, cell); newPtr.getCellRef().unsetRefNum(); // This RefNum is only valid within the original cell of the reference newPtr.getCellRef().setCount(count); newPtr.getRefData().setLuaScripts(nullptr); MWBase::Environment::get().getWorldModel()->registerPtr(newPtr); return newPtr; } MWWorld::Ptr Class::moveToCell(const Ptr& ptr, CellStore& cell) const { 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 { return false; } bool Class::canFly(const ConstPtr& ptr) const { return false; } bool Class::canSwim(const ConstPtr& ptr) const { return false; } 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); } bool Class::isPureFlyingCreature(const ConstPtr& ptr) const { 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); } bool Class::isMobile(const MWWorld::Ptr& ptr) const { return canSwim(ptr) || canWalk(ptr) || canFly(ptr); } 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 { throw std::runtime_error("class does not support gore"); } void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {} void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {} 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, std::string_view className) const { return false; } 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 { throw std::runtime_error("this is not a door"); } float Class::getNormalizedEncumbrance(const Ptr& ptr) const { float capacity = getCapacity(ptr); float encumbrance = getEncumbrance(ptr); if (encumbrance == 0) return 0.f; if (capacity == 0) return 1.f; return encumbrance / capacity; } ESM::RefId Class::getSound(const MWWorld::ConstPtr&) const { return ESM::RefId(); } int Class::getBaseFightRating(const ConstPtr& ptr) const { throw std::runtime_error("class does not support fight rating"); } ESM::RefId Class::getPrimaryFaction(const MWWorld::ConstPtr& ptr) const { return ESM::RefId(); } int Class::getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const { return -1; } 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); const ESM::RefId& enchantmentName = item.getClass().getEnchantment(item); if (enchantmentName.empty()) return result; const ESM::Enchantment* enchantment = MWBase::Environment::get().getESMStore()->get().search(enchantmentName); if (!enchantment || enchantment->mEffects.mList.empty()) return result; const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().search( enchantment->mEffects.mList.front().mData.mEffectID); if (!magicEffect) return result; result.x() = magicEffect->mData.mRed / 255.f; result.y() = magicEffect->mData.mGreen / 255.f; result.z() = magicEffect->mData.mBlue / 255.f; return result; } void Class::setBaseAISetting(const ESM::RefId&, MWMechanics::AiSetting setting, int value) const { throw std::runtime_error("class does not have creature stats"); } 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"); } float Class::getWalkSpeed(const Ptr& /*ptr*/) const { return 0; } float Class::getRunSpeed(const Ptr& /*ptr*/) const { return 0; } float Class::getSwimSpeed(const Ptr& /*ptr*/) const { return 0; } } openmw-openmw-0.49.0/apps/openmw/mwworld/class.hpp000066400000000000000000000423021503074453300222020ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CLASS_H #define GAME_MWWORLD_CLASS_H #include #include #include #include #include #include #include "doorstate.hpp" #include "ptr.hpp" #include "../mwmechanics/aisetting.hpp" #include "../mwmechanics/damagesourcetype.hpp" #include #include #include namespace ESM { struct ObjectState; } namespace MWRender { class RenderingInterface; } namespace MWPhysics { class PhysicsSystem; } namespace MWMechanics { class NpcStats; struct Movement; class CreatureStats; } namespace MWGui { struct ToolTipInfo; } namespace ESM { struct Position; } namespace MWWorld { class ContainerStore; class InventoryStore; class CellStore; class Action; /// \brief Base class for referenceable esm records class Class { const unsigned mType; static std::map& getClasses(); protected: explicit Class(unsigned type) : mType(type) { } std::unique_ptr defaultItemActivate(const Ptr& ptr, const Ptr& actor) const; ///< Generate default action for activating inventory items virtual Ptr copyToCellImpl(const ConstPtr& ptr, CellStore& cell) const; public: virtual ~Class() = default; Class(const Class&) = delete; Class& operator=(const Class&) = delete; unsigned int getType() const { return mType; } 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; virtual std::string_view getName(const ConstPtr& ptr) const = 0; ///< \return name or ID; can return an empty string. 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 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 bool hasToolTip(const ConstPtr& ptr) const; ///< @return true if this object has a tooltip when focused (default implementation: true) 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 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::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 hasItemHealth(const ConstPtr& ptr) const; ///< \return Item health data available? (default implementation: false) virtual int getItemHealth(const ConstPtr& ptr) const; ///< Return current item health or throw an exception if class does not have item health virtual float getItemNormalizedHealth(const ConstPtr& ptr) const; ///< Return current item health re-scaled to maximum health 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 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 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 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 std::unique_ptr activate(const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). 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 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 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 bool hasInventoryStore(const ConstPtr& ptr) const; ///< Does this object have an inventory store, i.e. equipment slots? (default implementation: false) virtual bool canLock(const ConstPtr& 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) 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 ESM::RefId getScript(const ConstPtr& ptr) const; ///< Return name of the script attached to ptr (default implementation: return an empty /// string). virtual float getWalkSpeed(const Ptr& ptr) const; virtual float getRunSpeed(const Ptr& ptr) const; virtual float getSwimSpeed(const Ptr& ptr) const; /// Return maximal movement speed for the current state. virtual float getMaxSpeed(const Ptr& ptr) const; /// Return current movement speed. virtual float getCurrentSpeed(const Ptr& ptr) const; virtual float getJump(const MWWorld::Ptr& ptr) const; ///< Return jump velocity (not accounting for movement) virtual MWMechanics::Movement& getMovementSettings(const Ptr& ptr) const; ///< Return desired movement. virtual osg::Vec3f getRotationVector(const Ptr& ptr) const; ///< Return desired rotations, as euler angles. Sets getMovementSettings(ptr).mRotation to zero. 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 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 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 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 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 float getNormalizedEncumbrance(const MWWorld::Ptr& ptr) const; ///< Returns encumbrance re-scaled to capacity virtual bool consume(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) const; ///< Consume an item, e. g. a potion. 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 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 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 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 ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const; ///< Returns the sound ID for \a ptr of the given soundgen \a type. virtual float getArmorRating(const MWWorld::Ptr& ptr) const; ///< @return combined armor rating of this actor virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const; ///< Return name of inventory icon. 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 int getEnchantmentPoints(const MWWorld::ConstPtr& ptr) const; ///< @return the number of enchantment points available for possible enchanting 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 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 int getServices(const MWWorld::ConstPtr& actor) const; virtual std::string_view getModel(const MWWorld::ConstPtr& ptr) const; virtual VFS::Path::Normalized getCorrectedModel(const MWWorld::ConstPtr& ptr) const; virtual bool useAnim() const; ///< Whether or not to use animated variant of model (default false) 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 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 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 float getWeight(const MWWorld::ConstPtr& ptr) const; virtual bool isPersistent(const MWWorld::ConstPtr& ptr) const; virtual bool isKey(const MWWorld::ConstPtr& ptr) const { return false; } virtual bool isGold(const MWWorld::ConstPtr& ptr) const { return false; } virtual bool isSoulGem(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 /// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini) virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const; virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const; // Similar to `copyToCell`, but preserves RefNum and moves LuaScripts. // 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; Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, const ESM::Position& pos, int count) const; virtual bool isActivator() const { return false; } virtual bool isActor() const { return false; } virtual bool isNpc() 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 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 writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const; ///< Write additional state from \a ptr into \a state. static const Class& get(unsigned int key); ///< If there is no class for this \a key, an exception is thrown. static void registerClass(Class& instance); virtual int getBaseGold(const MWWorld::ConstPtr& ptr) 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 void respawn(const MWWorld::Ptr& ptr) const {} /// Returns sound id virtual ESM::RefId getSound(const MWWorld::ConstPtr& ptr) const; virtual int getBaseFightRating(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; virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const; virtual void setBaseAISetting(const ESM::RefId& id, MWMechanics::AiSetting setting, int value) const; virtual void modifyBaseInventory(const ESM::RefId& actorId, const ESM::RefId& itemId, int amount) const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/containerstore.cpp000066400000000000000000001365641503074453300241450ustar00rootroot00000000000000#include "containerstore.hpp" #include "inventorystore.hpp" #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwmechanics/levelledlist.hpp" #include "../mwmechanics/recharge.hpp" #include "../mwmechanics/spellutil.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) { const auto& script = ptr.getClass().getScript(ptr); if (!script.empty()) { MWWorld::Ptr item = ptr; item.mCell = cell; scripts.add(script, item); } } } template float getTotalWeight(const MWWorld::CellRefList& cellRefList) { float sum = 0; for (const MWWorld::LiveCellRef& liveCellRef : cellRefList.mList) { 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 ESM::RefId& id, MWWorld::ContainerStore* store) { store->resolve(); for (MWWorld::LiveCellRef& liveCellRef : list.mList) { if ((liveCellRef.mBase->mId == id) && liveCellRef.mRef.getCount()) { MWWorld::Ptr ptr(&liveCellRef, nullptr); ptr.setContainerStore(store); return ptr; } } return MWWorld::Ptr(); } } MWWorld::ResolutionListener::~ResolutionListener() { try { mStore.unresolve(); } 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) { if (!LiveCellRef::checkState(state)) return ContainerStoreIterator(this); // not valid anymore with current content files -> skip const T* record = MWBase::Environment::get().getESMStore()->get().search(state.mRef.mRefID); if (!record) return ContainerStoreIterator(this); LiveCellRef ref(ESM::makeBlankCellRef(), record); ref.load(state); collection.mList.push_back(std::move(ref)); auto it = ContainerStoreIterator(this, --collection.mList.end()); MWBase::Environment::get().getWorldModel()->registerPtr(*it); return it; } void MWWorld::ContainerStore::storeEquipmentState( const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { } 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 { ref.save(state); } template void MWWorld::ContainerStore::storeStates( const CellRefList& collection, ESM::InventoryState& inventory, size_t& index, bool equipable) const { for (const LiveCellRef& liveCellRef : collection.mList) { if (liveCellRef.mRef.getCount() == 0) continue; ESM::ObjectState state; storeState(liveCellRef, state); if (equipable) storeEquipmentState(liveCellRef, index, inventory); inventory.mItems.push_back(std::move(state)); ++index; } } const ESM::RefId MWWorld::ContainerStore::sGoldId = ESM::RefId::stringRefId("gold_001"); MWWorld::ContainerStore::ContainerStore() : mListener(nullptr) , mRechargingItemsUpToDate(false) , mCachedWeight(0) , mWeightUpToDate(false) , mModified(false) , mResolved(false) , mSeed() , mPtr() { } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cbegin(int mask) const { return ConstContainerStoreIterator(mask, this); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::cend() const { return ConstContainerStoreIterator(this); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::begin(int mask) const { return cbegin(mask); } MWWorld::ConstContainerStoreIterator MWWorld::ContainerStore::end() const { return cend(); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::begin(int mask) { return ContainerStoreIterator(mask, this); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::end() { return ContainerStoreIterator(this); } int MWWorld::ContainerStore::count(const ESM::RefId& id) const { int total = 0; for (const auto&& iter : *this) 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, int count) { resolve(); if (ptr.getCellRef().getCount() <= count) return end(); 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.getCellRef().getCount() - count); return it; } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::restack(const MWWorld::Ptr& item) { resolve(); MWWorld::ContainerStoreIterator retval = end(); for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (item == *iter) { retval = iter; break; } } if (retval == end()) throw std::runtime_error("item is not from this container"); for (MWWorld::ContainerStoreIterator iter(begin()); iter != end(); ++iter) { if (stacks(*iter, item)) { iter->getCellRef().setCount( addItems(iter->getCellRef().getCount(false), item.getCellRef().getCount(false))); item.getCellRef().setCount(0); retval = iter; break; } } return retval; } bool MWWorld::ContainerStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { const MWWorld::Class& cls1 = ptr1.getClass(); const MWWorld::Class& cls2 = ptr2.getClass(); 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().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; } return ptr1 != ptr2 // an item never stacks onto itself && ptr1.getCellRef().getSoul() == ptr2.getCellRef().getSoul() && ptr1.getClass().getRemainingUsageTime(ptr1) == ptr2.getClass().getRemainingUsageTime(ptr2) // Items with scripts never stack && cls1.getScript(ptr1).empty() && 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))); } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::add(const ESM::RefId& id, int count, bool allowAutoEquip) { 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, bool /*allowAutoEquip*/, bool resolve) { 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 ESM::Position pos; pos.rot[0] = 0; pos.rot[1] = 0; pos.rot[2] = 0; pos.pos[0] = 0; pos.pos[1] = 0; pos.pos[2] = 0; 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(ESM::RefId()); item.getCellRef().resetGlobalVariable(); item.getCellRef().setFaction(ESM::RefId()); item.getCellRef().setFactionRank(-2); const ESM::RefId& script = item.getClass().getScript(item); if (!script.empty()) { 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; } else { // 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 if (!contPtr.isEmpty()) item.mCell = contPtr.getCell(); } item.mContainerStore = this; MWBase::Environment::get().getWorld()->getLocalScripts().add(script, item); // Set OnPCAdd special variable, if it is declared // Make sure to do this *after* we have added the script to LocalScripts 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 && typeid(*this) == typeid(ContainerStore)) mListener->itemAdded(item, count); return it; } MWWorld::ContainerStoreIterator MWWorld::ContainerStore::addImp(const Ptr& ptr, int count, bool markModified) { if (markModified) resolve(); int type = getType(ptr); 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) // 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)) { for (MWWorld::ContainerStoreIterator iter(begin(type)); iter != end(); ++iter) { if (iter->getCellRef().getRefId() == MWWorld::ContainerStore::sGoldId) { iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; } } 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) { // Don't stack with equipped items if (auto* inventoryStore = dynamic_cast(this)) if (inventoryStore->isEquipped(*iter)) continue; if (stacks(*iter, ptr)) { // stack iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); flagAsModified(); return iter; } } // if we got here, this means no stacking return addNewStack(ptr, 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; } it->getCellRef().setCount(count); flagAsModified(); return it; } void MWWorld::ContainerStore::rechargeItems(float duration) { if (!mRechargingItemsUpToDate) { updateRechargingItems(); mRechargingItemsUpToDate = true; } for (auto& it : mRechargingItems) { if (!MWMechanics::rechargeItem(*it.first, it.second, duration)) continue; // attempt to restack when fully recharged if (it.first->getCellRef().getEnchantmentCharge() == it.second) it.first = restack(*it.first); } } void MWWorld::ContainerStore::updateRechargingItems() { mRechargingItems.clear(); for (ContainerStoreIterator it = begin(); it != end(); ++it) { const auto& enchantmentId = it->getClass().getEnchantment(*it); if (!enchantmentId.empty()) { 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(); continue; } if (enchantment->mData.mType == ESM::Enchantment::WhenUsed || enchantment->mData.mType == ESM::Enchantment::WhenStrikes) mRechargingItems.emplace_back(it, static_cast(MWMechanics::getEnchantmentCharge(*enchantment))); } } } int MWWorld::ContainerStore::remove(const ESM::RefId& itemId, int count, bool equipReplacement, bool resolveFirst) { if (resolveFirst) resolve(); int toRemove = count; for (ContainerStoreIterator iter(begin()); iter != end() && toRemove > 0; ++iter) if (iter->getCellRef().getRefId() == itemId) toRemove -= remove(*iter, toRemove, equipReplacement, resolveFirst); flagAsModified(); // number of removed items return count - toRemove; } bool MWWorld::ContainerStore::hasVisibleItems() const { for (const auto&& iter : *this) { if (iter.getClass().showsInInventory(iter)) return true; } return false; } int MWWorld::ContainerStore::remove(const Ptr& item, int count, bool equipReplacement, bool resolveFirst) { assert(this == item.getContainerStore()); if (resolveFirst) resolve(); int toRemove = count; CellRef& itemRef = item.getCellRef(); if (itemRef.getCount() <= toRemove) { toRemove -= itemRef.getCount(); itemRef.setCount(0); } else { itemRef.setCount(subtractItems(itemRef.getCount(false), toRemove)); toRemove = 0; } flagAsModified(); // we should not fire event for InventoryStore yet - it has some custom logic 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 ESM::RefId& owner, Misc::Rng::Generator& prng) { for (const ESM::ContItem& iter : items.mList) { addInitialItem(iter.mItem, owner, iter.mCount, &prng); } flagAsModified(); mResolved = true; } void MWWorld::ContainerStore::fillNonRandom(const ESM::InventoryList& items, const ESM::RefId& owner, unsigned int seed) { mSeed = seed; for (const ESM::ContItem& iter : items.mList) { addInitialItem(iter.mItem, owner, iter.mCount, nullptr); } flagAsModified(); mResolved = false; } 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. try { ManualRef ref(*MWBase::Environment::get().getESMStore(), id, count); if (ref.getPtr().getClass().getScript(ref.getPtr()).empty()) { 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, prng, topLevel); } } catch (const std::exception& e) { Log(Debug::Warning) << "Warning: MWWorld::ContainerStore::addInitialItem: " << e.what(); } } void MWWorld::ContainerStore::addInitialItemImp( const MWWorld::Ptr& ptr, const ESM::RefId& owner, int count, Misc::Rng::Generator* prng, bool topLevel) { if (ptr.getType() == ESM::ItemLevList::sRecordId) { 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 < std::abs(count); ++i) addInitialItem(ptr.getCellRef().getRefId(), owner, count > 0 ? 1 : -1, prng, true); return; } else { const auto& itemId = MWMechanics::getLevelledItem(ptr.get()->mBase, false, *prng); if (itemId.empty()) return; addInitialItem(itemId, owner, count, prng, false); } } else { ptr.getCellRef().setOwner(owner); MWWorld::ContainerStoreIterator it = addImp(ptr, count, false); MWBase::Environment::get().getWorldModel()->registerPtr(*it); } } void MWWorld::ContainerStore::clear() { for (auto&& iter : *this) iter.getCellRef().setCount(0); flagAsModified(); mModified = true; } void MWWorld::ContainerStore::flagAsModified() { mWeightUpToDate = false; mRechargingItemsUpToDate = false; } bool MWWorld::ContainerStore::isResolved() const { return mResolved; } void MWWorld::ContainerStore::resolve() { const Ptr& container = getPtr(); if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { 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) return {}; std::shared_ptr listener = mResolutionListener.lock(); if (!listener) { listener = std::make_shared(*this); mResolutionListener = listener; } const Ptr& container = getPtr(); if (!mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { 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 { std::move(listener) }; } void MWWorld::ContainerStore::unresolve() { if (mModified) return; const Ptr& container = getPtr(); if (mResolved && !container.isEmpty() && container.getType() == ESM::REC_CONT) { for (const auto&& ptr : *this) ptr.getCellRef().setCount(0); fillNonRandom(container.get()->mBase->mInventory, ESM::RefId(), mSeed); addScripts(*this, container.mCell); mResolved = false; } } float MWWorld::ContainerStore::getWeight() const { if (!mWeightUpToDate) { 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); mWeightUpToDate = true; } return mCachedWeight; } int MWWorld::ContainerStore::getType(const ConstPtr& ptr) { if (ptr.isEmpty()) throw std::runtime_error("can't put a non-existent object into a container"); if (ptr.getType() == ESM::Potion::sRecordId) return Type_Potion; if (ptr.getType() == ESM::Apparatus::sRecordId) return Type_Apparatus; if (ptr.getType() == ESM::Armor::sRecordId) return Type_Armor; if (ptr.getType() == ESM::Book::sRecordId) return Type_Book; if (ptr.getType() == ESM::Clothing::sRecordId) return Type_Clothing; if (ptr.getType() == ESM::Ingredient::sRecordId) return Type_Ingredient; if (ptr.getType() == ESM::Light::sRecordId) return Type_Light; if (ptr.getType() == ESM::Lockpick::sRecordId) return Type_Lockpick; if (ptr.getType() == ESM::Miscellaneous::sRecordId) return Type_Miscellaneous; if (ptr.getType() == ESM::Probe::sRecordId) return Type_Probe; if (ptr.getType() == ESM::Repair::sRecordId) return Type_Repair; if (ptr.getType() == ESM::Weapon::sRecordId) return Type_Weapon; 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 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 (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)) { item = iter; itemHealth = iterHealth; } } } return item; } MWWorld::Ptr MWWorld::ContainerStore::search(const ESM::RefId& id) { resolve(); { Ptr ptr = searchId(potions, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(appas, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(armors, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(books, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(clothes, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(ingreds, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(lights, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(lockpicks, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(miscItems, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(probes, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(repairs, id, this); if (!ptr.isEmpty()) return ptr; } { Ptr ptr = searchId(weapons, id, this); if (!ptr.isEmpty()) return ptr; } return Ptr(); } int MWWorld::ContainerStore::addItems(int count1, int count2) { int sum = std::abs(count1) + std::abs(count2); if (count1 < 0 || count2 < 0) return -sum; return sum; } int MWWorld::ContainerStore::subtractItems(int count1, int count2) { int sum = std::abs(count1) - std::abs(count2); if (count1 < 0 || count2 < 0) return -sum; return sum; } void MWWorld::ContainerStore::writeState(ESM::InventoryState& state) const { state.mItems.clear(); 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) { clear(); mModified = true; mResolved = true; size_t index = 0; for (const ESM::ObjectState& state : inventory.mItems) { int type = MWBase::Environment::get().getESMStore()->find(state.mRef.mRefID); 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 0: 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; break; } } } template template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src) { mType = src.mType; mMask = src.mMask; mContainer = src.mContainer; mPtr = src.mPtr; 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); } } template void MWWorld::ContainerStoreIteratorBase::incType() { if (mType == 0) mType = 1; else if (mType != -1) { mType <<= 1; if (mType > ContainerStore::Type_Last) mType = -1; } } template void MWWorld::ContainerStoreIteratorBase::nextType() { while (mType != -1) { incType(); if ((mType & mMask) && mType > 0) if (resetIterator()) break; } } template bool MWWorld::ContainerStoreIteratorBase::resetIterator() { switch (mType) { case ContainerStore::Type_Potion: mPotion = mContainer->potions.mList.begin(); return mPotion != mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: mApparatus = mContainer->appas.mList.begin(); return mApparatus != mContainer->appas.mList.end(); case ContainerStore::Type_Armor: mArmor = mContainer->armors.mList.begin(); return mArmor != mContainer->armors.mList.end(); case ContainerStore::Type_Book: mBook = mContainer->books.mList.begin(); return mBook != mContainer->books.mList.end(); case ContainerStore::Type_Clothing: mClothing = mContainer->clothes.mList.begin(); return mClothing != mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: mIngredient = mContainer->ingreds.mList.begin(); return mIngredient != mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: mLight = mContainer->lights.mList.begin(); return mLight != mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: mLockpick = mContainer->lockpicks.mList.begin(); return mLockpick != mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: mMiscellaneous = mContainer->miscItems.mList.begin(); return mMiscellaneous != mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: mProbe = mContainer->probes.mList.begin(); return mProbe != mContainer->probes.mList.end(); case ContainerStore::Type_Repair: mRepair = mContainer->repairs.mList.begin(); return mRepair != mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: mWeapon = mContainer->weapons.mList.begin(); return mWeapon != mContainer->weapons.mList.end(); } return false; } template bool MWWorld::ContainerStoreIteratorBase::incIterator() { switch (mType) { case ContainerStore::Type_Potion: ++mPotion; return mPotion == mContainer->potions.mList.end(); case ContainerStore::Type_Apparatus: ++mApparatus; return mApparatus == mContainer->appas.mList.end(); case ContainerStore::Type_Armor: ++mArmor; return mArmor == mContainer->armors.mList.end(); case ContainerStore::Type_Book: ++mBook; return mBook == mContainer->books.mList.end(); case ContainerStore::Type_Clothing: ++mClothing; return mClothing == mContainer->clothes.mList.end(); case ContainerStore::Type_Ingredient: ++mIngredient; return mIngredient == mContainer->ingreds.mList.end(); case ContainerStore::Type_Light: ++mLight; return mLight == mContainer->lights.mList.end(); case ContainerStore::Type_Lockpick: ++mLockpick; return mLockpick == mContainer->lockpicks.mList.end(); case ContainerStore::Type_Miscellaneous: ++mMiscellaneous; return mMiscellaneous == mContainer->miscItems.mList.end(); case ContainerStore::Type_Probe: ++mProbe; return mProbe == mContainer->probes.mList.end(); case ContainerStore::Type_Repair: ++mRepair; return mRepair == mContainer->repairs.mList.end(); case ContainerStore::Type_Weapon: ++mWeapon; return mWeapon == mContainer->weapons.mList.end(); } return true; } template template bool MWWorld::ContainerStoreIteratorBase::isEqual(const ContainerStoreIteratorBase& other) const { if (mContainer != other.mContainer) return false; 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; } return false; } template PtrType* MWWorld::ContainerStoreIteratorBase::operator->() const { mPtr = **this; return &mPtr; } 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; } if (ptr.isEmpty()) throw std::runtime_error("invalid iterator"); ptr.setContainerStore(mContainer); return ptr; } template MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator++() { do { if (incIterator()) nextType(); } while (mType != -1 && !(**this).getCellRef().getCount()); return *this; } template MWWorld::ContainerStoreIteratorBase MWWorld::ContainerStoreIteratorBase::operator++(int) { ContainerStoreIteratorBase iter(*this); ++*this; return iter; } template MWWorld::ContainerStoreIteratorBase& MWWorld::ContainerStoreIteratorBase::operator=( const ContainerStoreIteratorBase& rhs) { if (this != &rhs) { copy(rhs); } return *this; } template int MWWorld::ContainerStoreIteratorBase::getType() const { return mType; } 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(int mask, ContainerStoreType container) : mType(0) , mMask(mask) , mContainer(container) { nextType(); 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_Armor) , mMask(MWWorld::ContainerStore::Type_All) , mContainer(container) , mArmor(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_Clothing) , mMask(MWWorld::ContainerStore::Type_All) , mContainer(container) , mClothing(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_Light) , mMask(MWWorld::ContainerStore::Type_All) , mContainer(container) , mLight(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_Miscellaneous) , mMask(MWWorld::ContainerStore::Type_All) , mContainer(container) , mMiscellaneous(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_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) { return left.isEqual(right); } template bool MWWorld::operator!=(const ContainerStoreIteratorBase& left, const ContainerStoreIteratorBase& 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 void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); template void MWWorld::ContainerStoreIteratorBase::copy(const ContainerStoreIteratorBase& src); template void MWWorld::ContainerStoreIteratorBase::copy( const ContainerStoreIteratorBase& src); openmw-openmw-0.49.0/apps/openmw/mwworld/containerstore.hpp000066400000000000000000000377411503074453300241470ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CONTAINERSTORE_H #define GAME_MWWORLD_CONTAINERSTORE_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cellreflist.hpp" #include "ptr.hpp" namespace ESM { struct InventoryList; struct InventoryState; } namespace MWClass { class Container; } namespace MWWorld { class ContainerStore; template class ContainerStoreIteratorBase; typedef ContainerStoreIteratorBase ContainerStoreIterator; typedef ContainerStoreIteratorBase ConstContainerStoreIterator; class ResolutionListener { ContainerStore& mStore; public: ResolutionListener(ContainerStore& store) : mStore(store) { } ~ResolutionListener(); }; class 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; }; class ContainerStore { 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(); // (item, max charge) typedef std::vector> TRechargingItems; TRechargingItems mRechargingItems; bool mRechargingItemsUpToDate; 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::SafePtr mPtr; // Container or actor that holds this store. 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); template ContainerStoreIterator getState(CellRefList& collection, const ESM::ObjectState& state); 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; void updateRechargingItems(); virtual void storeEquipmentState( const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const; virtual void readEquipmentState( const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory); public: ContainerStore(); virtual ~ContainerStore() = default; virtual std::unique_ptr clone() { auto res = std::make_unique(*this); res->updateRefNums(); return res; } // Container or actor that holds this store. const Ptr& getPtr() const { return mPtr.ptrOrEmpty(); } void setPtr(const Ptr& ptr) { mPtr = SafePtr(ptr); } 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(); bool hasVisibleItems() const; 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 ESM::RefId& id, int count, bool allowAutoEquip = true); ///< Utility to construct a ManualRef and call add(ptr, count, actorPtr, true) 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, 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. 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. int count(const ESM::RefId& id) const; ///< @return How many items with refID \a id are in this container? 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) 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: 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 ESM::RefId& owner, Misc::Rng::Generator& seed); ///< Insert items into *this. 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. 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. Ptr findReplacement(const ESM::RefId& id); ///< Returns replacement for object with given id. Prefer used items (with low durability left). Ptr search(const ESM::RefId& id); virtual void writeState(ESM::InventoryState& state) const; virtual void readState(const ESM::InventoryState& state); bool isResolved() const; void resolve(); ResolutionHandle resolveTemporarily(); void unresolve(); friend class ContainerStoreIteratorBase; friend class ContainerStoreIteratorBase; friend class ResolutionListener; friend class MWClass::Container; }; template class ContainerStoreIteratorBase { template struct IsConvertible { static constexpr bool value = true; }; template struct IsConvertible { static constexpr bool value = false; }; template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::iterator type; }; template struct IteratorTrait { typedef typename MWWorld::CellRefList::List::const_iterator type; }; template struct Iterator : IteratorTrait { }; template struct ContainerStoreTrait { typedef ContainerStore* type; }; template struct ContainerStoreTrait { typedef const ContainerStore* type; }; typedef typename ContainerStoreTrait::type ContainerStoreType; int mType; int mMask; ContainerStoreType mContainer; mutable PtrType mPtr; typename Iterator::type mPotion; typename Iterator::type mApparatus; typename Iterator::type mArmor; typename Iterator::type mBook; typename Iterator::type mClothing; typename Iterator::type mIngredient; typename Iterator::type mLight; typename Iterator::type mLockpick; typename Iterator::type mMiscellaneous; typename Iterator::type mProbe; typename Iterator::type mRepair; typename Iterator::type mWeapon; ContainerStoreIteratorBase(ContainerStoreType container); ///< End-iterator 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(); ///< Reset iterator for selected type. /// /// \return Type not empty? bool incIterator(); ///< Increment iterator for selected type. /// /// \return reached the end? 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) { static_assert(IsConvertible::value); copy(other); } template bool isEqual(const ContainerStoreIteratorBase& other) const; PtrType* operator->() const; PtrType operator*() const; ContainerStoreIteratorBase& operator++(); ContainerStoreIteratorBase operator++(int); ContainerStoreIteratorBase& operator=(const ContainerStoreIteratorBase& rhs); ContainerStoreIteratorBase(const ContainerStoreIteratorBase& rhs) = default; int getType() const; const ContainerStore* getContainerStore() const; 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); } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/contentloader.hpp000066400000000000000000000006111503074453300237330ustar00rootroot00000000000000#ifndef CONTENTLOADER_HPP #define CONTENTLOADER_HPP #include namespace Loading { class Listener; } namespace MWWorld { struct ContentLoader { virtual ~ContentLoader() = default; virtual void load(const std::filesystem::path& filepath, int& index, Loading::Listener* listener) = 0; }; } /* namespace MWWorld */ #endif /* CONTENTLOADER_HPP */ openmw-openmw-0.49.0/apps/openmw/mwworld/customdata.cpp000066400000000000000000000057521503074453300232440ustar00rootroot00000000000000#include "customdata.hpp" #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()); } 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()); } 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()); } 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()); } 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()); } 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()); } } openmw-openmw-0.49.0/apps/openmw/mwworld/customdata.hpp000066400000000000000000000032651503074453300232460ustar00rootroot00000000000000#ifndef GAME_MWWORLD_CUSTOMDATA_H #define GAME_MWWORLD_CUSTOMDATA_H #include namespace MWClass { class CreatureCustomData; class ESM4NpcCustomData; class NpcCustomData; class ContainerCustomData; class DoorCustomData; class CreatureLevListCustomData; } namespace MWWorld { /// \brief Base class for the MW-class-specific part of RefData class CustomData { public: virtual ~CustomData() {} virtual std::unique_ptr clone() const = 0; // 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::NpcCustomData& asNpcCustomData(); virtual const MWClass::NpcCustomData& asNpcCustomData() const; virtual MWClass::ContainerCustomData& asContainerCustomData(); virtual const MWClass::ContainerCustomData& asContainerCustomData() const; virtual MWClass::DoorCustomData& asDoorCustomData(); virtual const MWClass::DoorCustomData& asDoorCustomData() 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)); } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/datetimemanager.cpp000066400000000000000000000154551503074453300242300ustar00rootroot00000000000000#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 "duration.hpp" #include "globals.hpp" #include "timestamp.hpp" namespace { static int getDaysPerMonth(int month) { 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; } throw std::runtime_error("month out of range"); } } namespace MWWorld { void DateTimeManager::setup(Globals& globalVariables) { 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) { if (hour < 0) hour = 0; const Duration duration = Duration::fromHours(hour); mGameHour = duration.getHours(); if (const int days = duration.getDays(); days > 0) setDay(days + mDay); } void DateTimeManager::setDay(int day) { if (day < 1) day = 1; int month = mMonth; while (true) { int days = getDaysPerMonth(month); if (day <= days) break; if (month < 11) { ++month; } else { month = 0; mYear++; } day -= days; } mDay = day; mMonth = month; } TimeStamp DateTimeManager::getTimeStamp() const { return TimeStamp(mGameHour, mDaysPassed); } void DateTimeManager::setGameTimeScale(float scale) { MWBase::Environment::get().getWorld()->setGlobalFloat(MWWorld::Globals::sTimeScale, scale); } ESM::EpochTimeStamp DateTimeManager::getEpochTimeStamp() const { ESM::EpochTimeStamp timeStamp; timeStamp.mGameHour = mGameHour; timeStamp.mDay = mDay; timeStamp.mMonth = mMonth; timeStamp.mYear = mYear; return timeStamp; } void DateTimeManager::setMonth(int month) { if (month < 0) month = 0; int years = month / 12; month = month % 12; int days = getDaysPerMonth(month); if (mDay > days) mDay = days; mMonth = month; if (years > 0) mYear += years; } void DateTimeManager::advanceTime(double hours, Globals& globalVariables) { hours += mGameHour; setHour(hours); int days = static_cast(hours / 24); if (days > 0) mDaysPassed += days; 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); } static std::vector getMonthNames() { 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; } std::string_view DateTimeManager::getMonthName(int month) const { static std::vector months = getMonthNames(); if (month == -1) month = mMonth; if (month < 0 || month >= static_cast(months.size())) return {}; else return months[month]; } void DateTimeManager::updateGlobalFloat(GlobalVariableName name, float value) { if (name == Globals::sGameHour) { setHour(value); } else if (name == Globals::sDay) { setDay(static_cast(value)); } else if (name == Globals::sMonth) { setMonth(static_cast(value)); } else if (name == Globals::sYear) { mYear = static_cast(value); } else if (name == Globals::sTimeScale) { mGameTimeScale = value; } else if (name == Globals::sDaysPassed) { mDaysPassed = static_cast(value); } } void DateTimeManager::updateGlobalInt(GlobalVariableName name, int value) { if (name == Globals::sGameHour) { setHour(static_cast(value)); } else if (name == Globals::sDay) { setDay(value); } else if (name == Globals::sMonth) { setMonth(value); } else if (name == Globals::sYear) { mYear = value; } else if (name == Globals::sTimeScale) { mGameTimeScale = static_cast(value); } else if (name == Globals::sDaysPassed) { mDaysPassed = value; } } 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; } } openmw-openmw-0.49.0/apps/openmw/mwworld/datetimemanager.hpp000066400000000000000000000054221503074453300242260ustar00rootroot00000000000000#ifndef GAME_MWWORLD_DATETIMEMANAGER_H #define GAME_MWWORLD_DATETIMEMANAGER_H #include #include #include "globalvariablename.hpp" namespace ESM { struct EpochTimeStamp; } namespace MWWorld { class Globals; class TimeStamp; class World; class DateTimeManager { public: // 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; 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 // 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); void updateGlobalInt(GlobalVariableName name, int value); void 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; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/doorstate.hpp000066400000000000000000000003031503074453300230740ustar00rootroot00000000000000#ifndef GAME_MWWORLD_DOORSTATE_H #define GAME_MWWORLD_DOORSTATE_H namespace MWWorld { enum class DoorState { Idle = 0, Opening = 1, Closing = 2, }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/duration.hpp000066400000000000000000000015451503074453300227260ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/esmloader.cpp000066400000000000000000000063311503074453300230450ustar00rootroot00000000000000#include "esmloader.hpp" #include "esmstore.hpp" #include #include #include #include #include #include #include #include #include #include "../mwbase/environment.hpp" namespace MWWorld { 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 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); const std::vector& parentIndices = reader->getParentFileIndices(); assert(reader->getGameFiles().size() == parentIndices.size()); for (std::size_t i = 0, n = parentIndices.size(); i < n; ++i) if (parentIndices[i] == 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, listener); break; } } mNameToIndex[Misc::StringUtils::lowerCase(Files::pathToUnicodeString(filepath.filename()))] = index; } } /* namespace MWWorld */ openmw-openmw-0.49.0/apps/openmw/mwworld/esmloader.hpp000066400000000000000000000020111503074453300230410ustar00rootroot00000000000000#ifndef ESMLOADER_HPP #define ESMLOADER_HPP #include #include #include #include "contentloader.hpp" namespace ToUTF8 { class Utf8Encoder; } namespace ESM { class ReadersCache; struct Dialogue; } namespace MWWorld { class ESMStore; 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 std::filesystem::path& filepath, int& index, Loading::Listener* listener) override; private: ESM::ReadersCache& mReaders; MWWorld::ESMStore& mStore; ToUTF8::Utf8Encoder* mEncoder; ESM::Dialogue* mDialogue; std::optional mMasterFileFormat; std::vector& mESMVersions; std::map mNameToIndex; }; } /* namespace MWWorld */ #endif // ESMLOADER_HPP openmw-openmw-0.49.0/apps/openmw/mwworld/esmstore.cpp000066400000000000000000000725651503074453300227470ustar00rootroot00000000000000#include "esmstore.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../mwmechanics/spelllist.hpp" namespace { struct Ref { ESM::RefNum mRefNum; std::size_t mRefID; 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::set& keyIDs, ESM::ReadersCache& readers) { // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); 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; bool deleted = false; while (cell.getNextRef(*reader, ref, deleted)) { if (deleted) refs.emplace_back(ref.mRefNum, deletedRefID); 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) { 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); } } } const ESM::RefId& getDefaultClass(const MWWorld::Store& classes) { auto it = classes.begin(); if (it != classes.end()) 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 std::vector npcsToReplace; for (const auto& npcIter : npcs) { ESM::NPC npc = npcIter.second; bool changed = false; const ESM::RefId& npcFaction = npc.mFaction; if (!npcFaction.empty()) { 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 = ESM::RefId(); npc.mNpdt.mRank = 0; changed = true; } } const ESM::Class* cls = classes.search(npc.mClass); 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; } 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) npcsToReplace.push_back(npc); } return npcsToReplace; } template std::vector getSpellsToReplace( const MWWorld::Store& spells, const MWWorld::Store& magicEffects) { std::vector spellsToReplace; for (RecordType spell : spells) { if (spell.mEffects.mList.empty()) continue; 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) { Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId << ": dropping invalid effect (index " << iter->mData.mEffectID << ")"; iter = spell.mEffects.mList.erase(iter); changed = true; continue; } if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mData.mAttribute != -1) { 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; } 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 (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(); } } } } namespace MWWorld { using IDMap = std::unordered_map; struct ESMStoreImp { ESMStore::StoreTuple mStores; std::map mRecNameToStore; // 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) { const std::size_t storeIndex = ESMStore::getTypeIndex(); if (stores.mStores.size() <= storeIndex) stores.mStores.resize(storeIndex + 1); assert(&store == &std::get>(stores.mStoreImp->mStores)); 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; } } } 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; } static bool readRecord(ESM4::Reader& reader, ESMStore& store) { return std::apply( [&reader](auto&... x) { return (typedReadRecordESM4(reader, x) || ...); }, store.mStoreImp->mStores); } }; int ESMStore::find(const ESM::RefId& id) const { IDMap::const_iterator it = mStoreImp->mIds.find(id); if (it == mStoreImp->mIds.end()) { return 0; } return it->second; } int ESMStore::findStatic(const ESM::RefId& id) const { IDMap::const_iterator it = mStoreImp->mStaticIds.find(id); if (it == mStoreImp->mStaticIds.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()); } ESMStore::~ESMStore() = default; void ESMStore::clearDynamic() { for (const auto& store : mDynamicStores) store->clearDynamic(); mStoreImp->mIds = mStoreImp->mStaticIds; movePlayerRecord(); } static bool isCacheableRecord(int id) { 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); // Loop through all records while (esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); if (esm.getRecordFlags() & ESM::FLAG_Ignored) { esm.skipRecord(); continue; } // 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 (recName == ESM::REC_INFO) { 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 { RecordId id = it->second->load(esm); if (id.mIsDeleted) { 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, Loading::Listener* listener) { if (listener != nullptr) listener->setProgressRange(::EsmLoader::fileProgress); auto visitorRec = [this, listener](ESM4::Reader& reader) { bool result = ESMStoreImp::readRecord(reader, *this); if (listener != nullptr) listener->setProgress(::EsmLoader::fileProgress * reader.getFileOffset() / reader.getFileSize()); return result; }; 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 { 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; } } 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); } std::vector enchantmentsToReplace = getSpellsToReplace(enchantments, magicEffects); for (const ESM::Enchantment& enchantment : enchantmentsToReplace) { enchantments.eraseStatic(enchantment.mId); enchantments.insertStatic(enchantment); } } void ESMStore::movePlayerRecord() { auto& npcs = getWritable(); auto player = npcs.find(ESM::RefId::stringRefId("Player")); npcs.insert(*player); } void ESMStore::validateDynamic() { auto& npcs = getWritable(); auto& scripts = getWritable(); 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) + 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 { writer.startRecord(ESM::REC_DYNA); writer.startSubRecord("COUN"); writer.writeT(mDynamicCount); writer.endRecord("COUN"); writer.endRecord(ESM::REC_DYNA); 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_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: case ESM::REC_CLOT: case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_LEVI: case ESM::REC_LEVC: case ESM::REC_LIGH: mStoreImp->mRecNameToStore[type]->read(reader); return true; case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: mStoreImp->mRecNameToStore[type]->read(reader, true); return true; case ESM::REC_DYNA: reader.getSubNameIs("COUN"); if (reader.getFormatVersion() <= ESM::MaxActiveSpellTypeVersion) { uint32_t dynamicCount32 = 0; reader.getHT(dynamicCount32); mDynamicCount = dynamicCount32; } else { reader.getHT(mDynamicCount); } return true; default: return false; } } void ESMStore::checkPlayer() { const ESM::NPC* player = get().find(ESM::RefId::stringRefId("Player")); 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 ESM::RefId& id) const { auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) ptr = result->second.lock(); if (!ptr) { int type = find(id); ptr = std::make_shared(id, type); if (result != mSpellListCache.end()) result->second = ptr; else 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); } 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 openmw-openmw-0.49.0/apps/openmw/mwworld/esmstore.hpp000066400000000000000000000237011503074453300227400ustar00rootroot00000000000000#ifndef OPENMW_MWWORLD_ESMSTORE_H #define OPENMW_MWWORLD_ESMSTORE_H #include #include #include #include #include #include #include #include #include #include "store.hpp" namespace Loading { class Listener; } 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 { 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 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::vector::const_iterator iterator; iterator begin() const { return mDynamicStores.begin(); } iterator end() const { return mDynamicStores.end(); } /// Look up the given ID in 'all'. Returns 0 if not found. int find(const ESM::RefId& id) const; int findStatic(const ESM::RefId& id) const; ESMStore(); ~ESMStore(); void clearDynamic(); void rebuildIdsIndex(); ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); } void movePlayerRecord(); /// Validate entries in store after loading a save void validateDynamic(); void load(ESM::ESMReader& esm, Loading::Listener* listener, ESM::Dialogue*& dialogue); void loadESM4(ESM4::Reader& esm, Loading::Listener* listener); template 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 ESM::RefId id = generateId(); Store& store = getWritable(); if (store.search(id) != nullptr) throw std::runtime_error("Try to override existing record: " + id.toDebugString()); T record = x; record.mId = id; 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 = 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) { Store& store = getWritable(); if (store.search(x.mId) != nullptr) throw std::runtime_error("Try to override existing record " + x.mId.toDebugString()); T* ptr = store.insertStatic(x); if constexpr (std::is_convertible_v*, DynamicStore*>) { 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(); void validateRecords(ESM::ReadersCache& readers); int countSavedGameRecords() const; void write(ESM::ESMWriter& writer, Loading::Listener& progress) const; 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 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 ESM::RefId& id) const; }; template <> const ESM::Cell* ESMStore::insert(const ESM::Cell& cell); template <> const ESM::NPC* ESMStore::insert(const ESM::NPC& npc); template > struct HasRecordId : std::false_type { }; template struct HasRecordId> : std::true_type { }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/failedaction.cpp000066400000000000000000000010131503074453300235040ustar00rootroot00000000000000#include "failedaction.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" #include "../mwmechanics/actorutil.hpp" namespace MWWorld { FailedAction::FailedAction(std::string_view msg, const Ptr& target) : Action(false, target) , mMessage(msg) { } void FailedAction::executeImp(const Ptr& actor) { if (actor == MWMechanics::getPlayer() && !mMessage.empty()) MWBase::Environment::get().getWindowManager()->messageBox(mMessage); } } openmw-openmw-0.49.0/apps/openmw/mwworld/failedaction.hpp000066400000000000000000000005721503074453300235220ustar00rootroot00000000000000#ifndef GAME_MWWORLD_FAILEDACTION_H #define GAME_MWWORLD_FAILEDACTION_H #include "action.hpp" #include "ptr.hpp" namespace MWWorld { class FailedAction : public Action { std::string_view mMessage; void executeImp(const Ptr& actor) override; public: FailedAction(std::string_view message = {}, const Ptr& target = Ptr()); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/globals.cpp000066400000000000000000000054461503074453300225230ustar00rootroot00000000000000#include "globals.hpp" #include #include #include #include "esmstore.hpp" namespace MWWorld { Globals::Collection::const_iterator Globals::find(std::string_view name) const { Collection::const_iterator iter = mVariables.find(name); if (iter == mVariables.end()) throw std::runtime_error("unknown global variable: " + std::string{ name }); return iter; } Globals::Collection::iterator Globals::find(std::string_view name) { Collection::iterator iter = mVariables.find(name); if (iter == mVariables.end()) throw std::runtime_error("unknown global variable: " + std::string{ name }); return iter; } void Globals::fill(const MWWorld::ESMStore& store) { mVariables.clear(); const MWWorld::Store& globals = store.get(); for (const ESM::Global& esmGlobal : globals) { mVariables.emplace(esmGlobal.mId, esmGlobal); } } const ESM::Variant& Globals::operator[](GlobalVariableName name) const { return find(name.getValue())->second.mValue; } ESM::Variant& Globals::operator[](GlobalVariableName name) { return find(name.getValue())->second.mValue; } char Globals::getType(GlobalVariableName name) const { Collection::const_iterator iter = mVariables.find(name.getValue()); 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 ' '; } } int Globals::countSavedGameRecords() const { return mVariables.size(); } void Globals::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { for (const auto& variable : mVariables) { writer.startRecord(ESM::REC_GLOB); variable.second.save(writer); writer.endRecord(ESM::REC_GLOB); } } bool Globals::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_GLOB) { ESM::Global global; bool isDeleted = false; // 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); if (const auto iter = mVariables.find(global.mId); iter != mVariables.end()) iter->second = std::move(global); return true; } return false; } } openmw-openmw-0.49.0/apps/openmw/mwworld/globals.hpp000066400000000000000000000047621503074453300225300ustar00rootroot00000000000000#ifndef GAME_MWWORLD_GLOBALS_H #define GAME_MWWORLD_GLOBALS_H #include #include #include #include #include #include #include "globalvariablename.hpp" namespace ESM { class ESMWriter; class ESMReader; } namespace Loading { class Listener; } namespace MWWorld { class ESMStore; class Globals { private: using Collection = std::map>; Collection mVariables; // type, value Collection::const_iterator find(std::string_view name) const; Collection::iterator find(std::string_view name); 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" }; const ESM::Variant& operator[](GlobalVariableName name) const; ESM::Variant& operator[](GlobalVariableName name); char getType(GlobalVariableName name) const; ///< If there is no global variable with this name, ' ' is returned. void fill(const MWWorld::ESMStore& store); ///< Replace variables with variables from \a store with default values. int countSavedGameRecords() const; 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? }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/globalvariablename.hpp000066400000000000000000000015471503074453300247120ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/groundcoverstore.cpp000066400000000000000000000042431503074453300245040ustar00rootroot00000000000000#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; ::EsmLoader::EsmData content = ::EsmLoader::loadEsmData(query, groundcoverFiles, fileCollections, readers, encoder, listener); static constexpr std::string_view prefix = "grass/"; for (const ESM::Static& stat : statics) { VFS::Path::Normalized model = VFS::Path::toNormalized(stat.mModel); if (!model.value().starts_with(prefix)) continue; mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } for (const ESM::Static& stat : content.mStatics) { VFS::Path::Normalized model = VFS::Path::toNormalized(stat.mModel); if (!model.value().starts_with(prefix)) continue; mMeshCache[stat.mId] = Misc::ResourceHelpers::correctMeshPath(model); } for (ESM::Cell& cell : content.mCells) { if (!cell.isExterior()) continue; auto cellIndex = std::make_pair(cell.getGridX(), cell.getGridY()); mCellContexts[cellIndex] = std::move(cell.mContextList); } } 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; } } openmw-openmw-0.49.0/apps/openmw/mwworld/groundcoverstore.hpp000066400000000000000000000023331503074453300245070ustar00rootroot00000000000000#ifndef GAME_MWWORLD_GROUNDCOVER_STORE_H #define GAME_MWWORLD_GROUNDCOVER_STORE_H #include #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); VFS::Path::NormalizedView getGroundcoverModel(ESM::RefId id) const { auto it = mMeshCache.find(id); if (it == mMeshCache.end()) return {}; return it->second; } void initCell(ESM::Cell& cell, int cellX, int cellY) const; }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/inventorystore.cpp000066400000000000000000000606111503074453300242050ustar00rootroot00000000000000#include "inventorystore.hpp" #include #include #include #include "../mwbase/environment.hpp" #include "../mwmechanics/actorutil.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/weapontype.hpp" #include "class.hpp" #include "esmstore.hpp" 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) { std::size_t distance = std::distance(const_cast(store).begin(), *iter); ContainerStoreIterator slot = begin(); std::advance(slot, distance); 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); ContainerStoreIterator slot = begin(); std::advance(slot, distance); mSelectedEnchantItem = slot; } void MWWorld::InventoryStore::initSlots(TSlots& slots_) { for (int i = 0; i < Slots; ++i) slots_.push_back(end()); } void MWWorld::InventoryStore::storeEquipmentState( const MWWorld::LiveCellRefBase& ref, size_t index, ESM::InventoryState& inventory) const { 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) inventory.mSelectedEnchantItem = index; } void MWWorld::InventoryStore::readEquipmentState( const MWWorld::ContainerStoreIterator& iter, size_t index, const ESM::InventoryState& inventory) { if (index == inventory.mSelectedEnchantItem) mSelectedEnchantItem = iter; 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 int32_t slot = found->second; std::pair, bool> allowedSlots = iter->getClass().getEquipmentSlots(*iter); if (!allowedSlots.first.size()) return; if (std::find(allowedSlots.first.begin(), allowedSlots.first.end(), slot) == allowedSlots.first.end()) slot = allowedSlots.first.front(); // unstack if required if (!allowedSlots.second && iter->getCellRef().getCount() > 1) { int count = iter->getCellRef().getCount(false); MWWorld::ContainerStoreIterator newIter = addNewStack(*iter, count > 0 ? 1 : -1); iter->getCellRef().setCount(subtractItems(count, 1)); mSlots[slot] = newIter; } else mSlots[slot] = iter; } } MWWorld::InventoryStore::InventoryStore() : ContainerStore() , mInventoryListener(nullptr) , mUpdatesEnabled(true) , mFirstAutoEquip(true) , mSelectedEnchantItem(end()) { initSlots(mSlots); } MWWorld::InventoryStore::InventoryStore(const InventoryStore& store) : ContainerStore(store) , mInventoryListener(store.mInventoryListener) , mUpdatesEnabled(store.mUpdatesEnabled) , mFirstAutoEquip(store.mFirstAutoEquip) , mSelectedEnchantItem(end()) { copySlots(store); } MWWorld::InventoryStore& MWWorld::InventoryStore::operator=(const InventoryStore& store) { if (this == &store) return *this; mListener = store.mListener; mInventoryListener = store.mInventoryListener; mFirstAutoEquip = store.mFirstAutoEquip; mRechargingItemsUpToDate = false; ContainerStore::operator=(store); mSlots.clear(); copySlots(store); return *this; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::add( const Ptr& itemPtr, int count, bool allowAutoEquip, bool 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 const Ptr& actor = getPtr(); if (allowAutoEquip && actor != MWMechanics::getPlayer() && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { auto type = itemPtr.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(); } if (mListener) mListener->itemAdded(*retVal, count); return retVal; } 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"); 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"); std::pair, bool> slots_; 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 (mSlots[slot] != end()) unequipSlot(slot); // unstack item pointed to by iterator if required if (iterator != end() && !slots_.second && iterator->getCellRef().getCount() > 1) // if slots.second is true, item can stay stacked when equipped { unstack(*iterator); } mSlots[slot] = iterator; flagAsModified(); fireEquipmentChangedEvent(); } void MWWorld::InventoryStore::unequipAll() { mUpdatesEnabled = false; for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) unequipSlot(slot); mUpdatesEnabled = true; fireEquipmentChangedEvent(); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) { return findSlot(slot); } MWWorld::ConstContainerStoreIterator MWWorld::InventoryStore::getSlot(int slot) const { return findSlot(slot); } 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 (mSlots[slot] == end()) return mSlots[slot]; // 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(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); if (sellsWeapon) return; } static const ESM::RefId weaponSkills[] = { ESM::Skill::LongBlade, ESM::Skill::Axe, ESM::Skill::Spear, ESM::Skill::ShortBlade, ESM::Skill::Marksman, ESM::Skill::BluntWeapon, }; const size_t weaponSkillsLength = sizeof(weaponSkills) / sizeof(weaponSkills[0]); bool weaponSkillVisited[weaponSkillsLength] = { false }; // give arrows/bolt with max damage by default int arrowMax = 0; int boltMax = 0; ContainerStoreIterator arrow(end()); ContainerStoreIterator bolt(end()); // rate ammo for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (esmWeapon->mData.mType == ESM::Weapon::Arrow) { if (esmWeapon->mData.mChop[1] >= arrowMax) { arrowMax = esmWeapon->mData.mChop[1]; arrow = iter; } } else if (esmWeapon->mData.mType == ESM::Weapon::Bolt) { if (esmWeapon->mData.mChop[1] >= boltMax) { boltMax = esmWeapon->mData.mChop[1]; bolt = iter; } } } // rate weapon for (int i = 0; i < static_cast(weaponSkillsLength); ++i) { float max = 0; int maxWeaponSkill = -1; for (int j = 0; j < static_cast(weaponSkillsLength); ++j) { float skillValue = actor.getClass().getSkill(actor, weaponSkills[j]); if (skillValue > max && !weaponSkillVisited[j]) { max = skillValue; maxWeaponSkill = j; } } if (maxWeaponSkill == -1) break; max = 0; ContainerStoreIterator weapon(end()); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Weapon)); iter != end(); ++iter) { const ESM::Weapon* esmWeapon = iter->get()->mBase; if (MWMechanics::getWeaponType(esmWeapon->mData.mType)->mWeaponClass == ESM::WeaponType::Ammo) continue; if (iter->getClass().getEquipmentSkill(*iter) == weaponSkills[maxWeaponSkill]) { if (esmWeapon->mData.mChop[1] >= max) { max = esmWeapon->mData.mChop[1]; weapon = iter; } if (esmWeapon->mData.mSlash[1] >= max) { max = esmWeapon->mData.mSlash[1]; weapon = iter; } if (esmWeapon->mData.mThrust[1] >= max) { max = esmWeapon->mData.mThrust[1]; weapon = iter; } } } if (weapon != end() && weapon->getClass().canBeEquipped(*weapon, actor).first) { // Do not equip ranged weapons, if there is no suitable ammo bool hasAmmo = true; const MWWorld::LiveCellRef* ref = weapon->get(); int type = ref->mBase->mData.mType; int ammotype = MWMechanics::getWeaponType(type)->mAmmoType; if (ammotype == ESM::Weapon::Arrow) { if (arrow == end()) hasAmmo = false; else slots_[Slot_Ammunition] = arrow; } else if (ammotype == ESM::Weapon::Bolt) { if (bolt == end()) hasAmmo = false; else slots_[Slot_Ammunition] = bolt; } if (hasAmmo) { std::pair, bool> itemsSlots = weapon->getClass().getEquipmentSlots(*weapon); if (!itemsSlots.first.empty()) { if (!itemsSlots.second) { if (weapon->getCellRef().getCount() > 1) { unstack(*weapon); } } int slot = itemsSlots.first.front(); slots_[slot] = weapon; if (ammotype == ESM::Weapon::None) slots_[Slot_Ammunition] = end(); } break; } } weaponSkillVisited[maxWeaponSkill] = true; } } 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(slots_); return; } const MWWorld::Store& store = MWBase::Environment::get().getESMStore()->get(); static float fUnarmoredBase1 = store.find("fUnarmoredBase1")->mValue.getFloat(); static float fUnarmoredBase2 = store.find("fUnarmoredBase2")->mValue.getFloat(); float unarmoredSkill = actor.getClass().getSkill(actor, ESM::Skill::Unarmored); float unarmoredRating = (fUnarmoredBase1 * unarmoredSkill) * (fUnarmoredBase2 * unarmoredSkill); for (ContainerStoreIterator iter(begin(ContainerStore::Type_Clothing | ContainerStore::Type_Armor)); iter != end(); ++iter) { Ptr test = *iter; switch (test.getClass().canBeEquipped(test, actor).first) { case 0: continue; default: break; } 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); // 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()) { Ptr old = *slots_.at(slot); if (iter.getType() == ContainerStore::Type_Armor) { 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)) // old armor had better armor rating continue; } } // suitable armor should replace already equipped clothing } else if (iter.getType() == ContainerStore::Type_Clothing) { // if left ring is equipped if (slot == Slot_LeftRing) { // if there is a place for right ring dont swap it if (slots_.at(Slot_RightRing) == end()) { continue; } else // if right ring is equipped too { Ptr rightRing = *slots_.at(Slot_RightRing); // we want to swap cheaper ring only if both are equipped if (old.getClass().getValue(old) >= rightRing.getClass().getValue(rightRing)) continue; } } if (old.getType() == ESM::Clothing::sRecordId) { // check value if (old.getClass().getValue(old) >= test.getClass().getValue(test)) // old clothing was more valuable continue; } else // suitable clothing should NOT replace already equipped armor continue; } } if (!itemsSlots.second) // if itemsSlots.second is true, item can stay stacked when equipped { // unstack item pointed to by iterator if required if (iter->getCellRef().getCount() > 1) { unstack(*iter); } } // if we are here it means item can be equipped or swapped slots_[slot] = iter; break; } } } void MWWorld::InventoryStore::autoEquipShield(TSlots& slots_) { for (ContainerStoreIterator iter(begin(ContainerStore::Type_Armor)); iter != end(); ++iter) { if (iter->get()->mBase->mData.mType != ESM::Armor::Shield) continue; if (iter->getClass().canBeEquipped(*iter, getPtr()).first != 1) continue; std::pair, bool> shieldSlots = iter->getClass().getEquipmentSlots(*iter); int slot = shieldSlots.first[0]; const ContainerStoreIterator& shield = slots_[slot]; if (shield != end() && shield.getType() == Type_Armor && shield->get()->mBase->mData.mType == ESM::Armor::Shield) { if (shield->getClass().getItemHealth(*shield) >= iter->getClass().getItemHealth(*iter)) continue; } slots_[slot] = iter; } } void MWWorld::InventoryStore::autoEquip() { TSlots slots_; initSlots(slots_); // Disable model update during auto-equip mUpdatesEnabled = false; // Autoequip clothing, armor and weapons. // Equipping lights is handled in Actors::updateEquippedLight based on environment light. // Note: creatures ignore equipment armor rating and only equip shields // Use custom logic for them - select shield based on its health instead of armor rating autoEquipWeapon(slots_); autoEquipArmor(slots_); bool changed = false; for (std::size_t i = 0; i < slots_.size(); ++i) { if (slots_[i] != mSlots[i]) { changed = true; break; } } mUpdatesEnabled = true; if (changed) { mSlots.swap(slots_); fireEquipmentChangedEvent(); flagAsModified(); } } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getPreferredShield() { TSlots slots; initSlots(slots); autoEquipArmor(slots); return slots[Slot_CarriedLeft]; } bool MWWorld::InventoryStore::stacks(const ConstPtr& ptr1, const ConstPtr& ptr2) const { bool canStack = MWWorld::ContainerStore::stacks(ptr1, ptr2); if (!canStack) return false; // don't stack if either item is currently equipped for (TSlots::const_iterator iter(mSlots.begin()); iter != mSlots.end(); ++iter) { if (*iter != end() && (ptr1 == **iter || ptr2 == **iter)) { bool stackWhenEquipped = (*iter)->getClass().getEquipmentSlots(**iter).second; if (!stackWhenEquipped) return false; } } return true; } void MWWorld::InventoryStore::setSelectedEnchantItem(const ContainerStoreIterator& iterator) { mSelectedEnchantItem = iterator; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::getSelectedEnchantItem() { return mSelectedEnchantItem; } int MWWorld::InventoryStore::remove(const Ptr& item, int count, bool equipReplacement, bool resolve) { int retCount = ContainerStore::remove(item, count, equipReplacement, resolve); bool wasEquipped = false; if (!item.getCellRef().getCount()) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { if (mSlots[slot] == end()) continue; if (*mSlots[slot] == item) { unequipSlot(slot); wasEquipped = true; break; } } } // 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) const Ptr& actor = getPtr(); if (equipReplacement && wasEquipped && (actor != MWMechanics::getPlayer()) && actor.getClass().isNpc() && !actor.getClass().getNpcStats(actor).isWerewolf()) { auto type = item.getType(); if (type == ESM::Armor::sRecordId || type == ESM::Clothing::sRecordId) autoEquip(); } if (item.getCellRef().getCount() == 0 && mSelectedEnchantItem != end() && *mSelectedEnchantItem == item) { mSelectedEnchantItem = end(); } if (mListener) mListener->itemRemoved(item, retCount); return retCount; } 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"); ContainerStoreIterator it = mSlots[slot]; if (it != end()) { ContainerStoreIterator retval = it; // empty this slot mSlots[slot] = end(); if (it->getCellRef().getCount()) { retval = restack(*it); if (getPtr() == MWMechanics::getPlayer()) { // Unset OnPCEquip Variable on item's script, if it has a script with that variable declared const ESM::RefId& script = it->getClass().getScript(*it); if (!script.empty()) (*it).getRefData().getLocals().setVarByInt(script, "onpcequip", 0); } if ((mSelectedEnchantItem != end()) && (mSelectedEnchantItem == it)) { mSelectedEnchantItem = end(); } } if (applyUpdates) { fireEquipmentChangedEvent(); } return retval; } return it; } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItem(const MWWorld::Ptr& item) { for (int slot = 0; slot < MWWorld::InventoryStore::Slots; ++slot) { MWWorld::ContainerStoreIterator equipped = getSlot(slot); if (equipped != end() && *equipped == item) return unequipSlot(slot); } throw std::runtime_error("attempt to unequip an item that is not currently equipped"); } MWWorld::ContainerStoreIterator MWWorld::InventoryStore::unequipItemQuantity(const Ptr& item, int count) { if (!isEquipped(item)) throw std::runtime_error("attempt to unequip an item that is not currently equipped"); if (count <= 0) 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.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) { if (stacks(*iter, item) && !isEquipped(*iter)) { iter->getCellRef().setCount(addItems(iter->getCellRef().getCount(false), count)); item.getCellRef().setCount(subtractItems(item.getCellRef().getCount(false), count)); return iter; } } return unstack(item, item.getCellRef().getCount() - count); } MWWorld::InventoryStoreListener* MWWorld::InventoryStore::getInvListener() const { return mInventoryListener; } void MWWorld::InventoryStore::setInvListener(InventoryStoreListener* listener) { mInventoryListener = listener; } void MWWorld::InventoryStore::fireEquipmentChangedEvent() { if (!mUpdatesEnabled) return; if (mInventoryListener) mInventoryListener->equipmentChanged(); // if player, update inventory window /* if (mActor == MWMechanics::getPlayer()) { MWBase::Environment::get().getWindowManager()->getInventoryWindow()->updateItemView(); } */ } void MWWorld::InventoryStore::clear() { mSlots.clear(); initSlots(mSlots); ContainerStore::clear(); } bool MWWorld::InventoryStore::isEquipped(const MWWorld::ConstPtr& item) { for (int i = 0; i < MWWorld::InventoryStore::Slots; ++i) { if (getSlot(i) != end() && *getSlot(i) == item) return true; } return false; } bool MWWorld::InventoryStore::isFirstEquip() { bool first = mFirstAutoEquip; mFirstAutoEquip = false; return first; } openmw-openmw-0.49.0/apps/openmw/mwworld/inventorystore.hpp000066400000000000000000000153151503074453300242130ustar00rootroot00000000000000#ifndef GAME_MWWORLD_INVENTORYSTORE_H #define GAME_MWWORLD_INVENTORYSTORE_H #include "containerstore.hpp" namespace ESM { struct MagicEffect; } namespace MWMechanics { class NpcStats; } namespace MWWorld { class InventoryStoreListener { public: /** * Fired when items are equipped or unequipped */ virtual void equipmentChanged() {} virtual ~InventoryStoreListener() = default; }; ///< \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: 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; typedef std::vector TSlots; TSlots mSlots; void autoEquipWeapon(TSlots& slots_); void autoEquipArmor(TSlots& slots_); void autoEquipShield(TSlots& slots_); // selected magic item (for using enchantments of type "Cast once" or "Cast when used") ContainerStoreIterator mSelectedEnchantItem; void copySlots(const InventoryStore& store); void initSlots(TSlots& slots_); void fireEquipmentChangedEvent(); 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; ContainerStoreIterator findSlot(int slot) const; 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(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/livecellref.cpp000066400000000000000000000104471503074453300233710ustar00rootroot00000000000000#include "livecellref.hpp" #include #include #include #include #include #include "../mwbase/environment.hpp" #include "../mwbase/luamanager.hpp" #include "class.hpp" #include "esmstore.hpp" #include "ptr.hpp" #include "worldmodel.hpp" namespace MWWorld { LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM::CellRef& cref) : mClass(&Class::get(type)) , mRef(cref) , mData(cref) { } LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::Reference& cref) : mClass(&Class::get(type)) , mRef(cref) , mData(cref) { } LiveCellRefBase::LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref) : mClass(&Class::get(type)) , mRef(cref) , mData(cref) { } LiveCellRefBase::LiveCellRefBase(LiveCellRefBase&& other) noexcept : mClass(other.mClass) , mRef(std::move(other.mRef)) , mData(std::move(other.mData)) , mWorldModel(std::exchange(other.mWorldModel, nullptr)) { } LiveCellRefBase::~LiveCellRefBase() { if (mWorldModel != nullptr) mWorldModel->deregisterLiveCellRef(*this); } LiveCellRefBase& LiveCellRefBase::operator=(LiveCellRefBase&& other) noexcept { mClass = other.mClass; mRef = std::move(other.mRef); mData = std::move(other.mData); mWorldModel = std::exchange(other.mWorldModel, nullptr); return *this; } void LiveCellRefBase::loadImp(const ESM::ObjectState& state) { mRef = CellRef(state.mRef); mData = RefData(state, mData.isDeletedByContentFile()); Ptr ptr(this); if (state.mHasLocals) { 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().getESMStore()->get().search(scriptId)) { try { 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(); } } } } mClass->readAdditionalState(ptr, state); 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(ESM::RefId()); } MWBase::Environment::get().getLuaManager()->loadLocalScripts(ptr, state.mLuaScripts); } void LiveCellRefBase::saveImp(ESM::ObjectState& state) const { mRef.writeState(state); ConstPtr ptr(this); mData.write(state, mClass->getScript(ptr)); MWBase::Environment::get().getLuaManager()->saveLocalScripts( Ptr(const_cast(this)), state.mLuaScripts); mClass->writeAdditionalState(ptr, state); } bool LiveCellRefBase::checkStateImp(const ESM::ObjectState& state) { return true; } unsigned int LiveCellRefBase::getType() const { return mClass->getType(); } bool LiveCellRefBase::isDeleted() const { return mData.isDeletedByContentFile() || mRef.getCount(false) == 0; } 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(); } } openmw-openmw-0.49.0/apps/openmw/mwworld/livecellref.hpp000066400000000000000000000136751503074453300234040ustar00rootroot00000000000000#ifndef GAME_MWWORLD_LIVECELLREF_H #define GAME_MWWORLD_LIVECELLREF_H #include "cellref.hpp" #include "refdata.hpp" #include namespace ESM { struct ObjectState; } namespace MWWorld { class Ptr; class ESMStore; class Class; class WorldModel; template struct LiveCellRef; /// Used to create pointers to hold any type of LiveCellRef<> object. struct LiveCellRefBase { const Class* mClass; /** Information about this instance, such as 3D location and rotation * and individual type-dependent data. */ CellRef mRef; /** runtime-data */ RefData mData; WorldModel* mWorldModel = nullptr; LiveCellRefBase(unsigned int type, const ESM::CellRef& cref); LiveCellRefBase(unsigned int type, const ESM4::Reference& cref); LiveCellRefBase(unsigned int type, const ESM4::ActorCharacter& cref); LiveCellRefBase(const LiveCellRefBase& other) = default; LiveCellRefBase(LiveCellRefBase&& other) noexcept; /* Need this for the class to be recognized as polymorphic */ virtual ~LiveCellRefBase(); LiveCellRefBase& operator=(const LiveCellRefBase& other) = default; LiveCellRefBase& operator=(LiveCellRefBase&& other) noexcept; 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; ///< Save LiveCellRef state into \a state. virtual std::string_view getTypeDescription() const = 0; unsigned int getType() const; ///< @see MWWorld::Class::getType template static const LiveCellRef* dynamicCast(const LiveCellRefBase* value); template static LiveCellRef* dynamicCast(LiveCellRefBase* value); /// Returns true if the object was either deleted by the content file or by gameplay. bool isDeleted() const; 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) { 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) { 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. /// /// Constructing this with a CellRef instance in the constructor means that /// in practice (where D is RefData) the possibly mutable data is copied /// across to mData. If later adding data (such as position) to CellRef /// this would have to be manually copied across. template struct LiveCellRef : public LiveCellRefBase { LiveCellRef(const ESM::CellRef& cref, const X* b = nullptr) : 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) { } // The object that this instance is based on. const X* mBase; 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; ///< Save LiveCellRef state into \a 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? /// /// \note Does not check if the RefId exists. }; template void LiveCellRef::load(const ESM::ObjectState& state) { loadImp(state); } template void LiveCellRef::save(ESM::ObjectState& state) const { saveImp(state); } template bool LiveCellRef::checkState(const ESM::ObjectState& state) { return checkStateImp(state); } } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/localscripts.cpp000066400000000000000000000114701503074453300235740ustar00rootroot00000000000000#include "localscripts.hpp" #include #include #include #include #include #include "cellstore.hpp" #include "class.hpp" #include "containerstore.hpp" #include "esmstore.hpp" namespace { struct AddScriptsVisitor { AddScriptsVisitor(MWWorld::LocalScripts& scripts) : mScripts(scripts) { } MWWorld::LocalScripts& mScripts; bool operator()(const MWWorld::Ptr& ptr) { if (ptr.mRef->isDeleted()) return true; const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) mScripts.add(script, ptr); return true; } }; struct AddContainerItemScriptsVisitor { AddContainerItemScriptsVisitor(MWWorld::LocalScripts& scripts) : mScripts(scripts) { } MWWorld::LocalScripts& mScripts; bool operator()(const MWWorld::Ptr& containerPtr) { // Ignore containers without generated content if (containerPtr.getType() == ESM::Container::sRecordId && containerPtr.getRefData().getCustomData() == nullptr) return true; MWWorld::ContainerStore& container = containerPtr.getClass().getContainerStore(containerPtr); for (const auto& ptr : container) { const ESM::RefId& script = ptr.getClass().getScript(ptr); if (!script.empty()) { MWWorld::Ptr item = ptr; item.mCell = containerPtr.getCell(); mScripts.add(script, item); } } return true; } }; } MWWorld::LocalScripts::LocalScripts(const MWWorld::ESMStore& store) : mStore(store) { mIter = mScripts.end(); } void MWWorld::LocalScripts::startIteration() { mIter = mScripts.begin(); } bool MWWorld::LocalScripts::getNext(std::pair& script) { if (mIter != mScripts.end()) { auto iter = mIter++; script = *iter; return true; } return false; } void MWWorld::LocalScripts::add(const ESM::RefId& scriptName, const Ptr& ptr) { if (const ESM::Script* script = mStore.get().search(scriptName)) { try { ptr.getRefData().setLocals(*script); 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); } catch (const std::exception& exception) { 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."; } void MWWorld::LocalScripts::addCell(CellStore* cell) { AddScriptsVisitor addScriptsVisitor(*this); cell->forEach(addScriptsVisitor); AddContainerItemScriptsVisitor addContainerItemScriptsVisitor(*this); cell->forEachType(addContainerItemScriptsVisitor); cell->forEachType(addContainerItemScriptsVisitor); cell->forEachType(addContainerItemScriptsVisitor); } void MWWorld::LocalScripts::clear() { mScripts.clear(); } void MWWorld::LocalScripts::clearCell(CellStore* cell) { auto iter = mScripts.begin(); while (iter != mScripts.end()) { if (iter->second.mCell == cell) { if (iter == mIter) ++mIter; mScripts.erase(iter++); } else ++iter; } } void MWWorld::LocalScripts::remove(const MWWorld::CellRef* ref) { for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) if (&(iter->second.getCellRef()) == ref) { if (iter == mIter) ++mIter; mScripts.erase(iter); break; } } void MWWorld::LocalScripts::remove(const Ptr& ptr) { for (auto iter = mScripts.begin(); iter != mScripts.end(); ++iter) if (iter->second == ptr) { if (iter == mIter) ++mIter; 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(); } openmw-openmw-0.49.0/apps/openmw/mwworld/localscripts.hpp000066400000000000000000000026341503074453300236030ustar00rootroot00000000000000#ifndef GAME_MWWORLD_LOCALSCRIPTS_H #define GAME_MWWORLD_LOCALSCRIPTS_H #include #include #include "ptr.hpp" namespace MWWorld { class ESMStore; class CellStore; class RefData; /// \brief List of active local scripts class LocalScripts { std::list> mScripts; std::list>::iterator mIter; const MWWorld::ESMStore& mStore; public: LocalScripts(const MWWorld::ESMStore& store); 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? void add(const ESM::RefId& scriptName, const Ptr& ptr); ///< Add script to collection of active local scripts. void addCell(CellStore* cell); ///< Add all local scripts in a cell. void clear(); ///< Clear active local scripts collection. void clearCell(CellStore* cell); ///< Remove all scripts belonging to \a cell. 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). bool isRunning(const ESM::RefId&, const Ptr&) const; ///< Is the local script running?. }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/magiceffects.cpp000066400000000000000000000261001503074453300235060ustar00rootroot00000000000000#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 = 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 = {}; } } } openmw-openmw-0.49.0/apps/openmw/mwworld/magiceffects.hpp000066400000000000000000000007661503074453300235250ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/manualref.cpp000066400000000000000000000077751503074453300230610ustar00rootroot00000000000000#include "manualref.hpp" #include #include #include "esmstore.hpp" namespace { 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.blank(); cellRef.mRefID = name; 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)"); default: throw std::logic_error( "failed to create manual cell ref for " + name.toDebugString() + " (unknown type)"); } } } MWWorld::ManualRef::ManualRef(const MWWorld::ESMStore& store, const ESM::RefId& name, const int count) { auto cb = [&](const auto& store) { create(store, name, mRef, mPtr); }; visitRefStore(store, name, cb); mPtr.getCellRef().setCount(count); } 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.getCellRef().setCount(count); mPtr.getCellRef().unsetRefNum(); mPtr.getRefData().setLuaScripts(nullptr); } openmw-openmw-0.49.0/apps/openmw/mwworld/manualref.hpp000066400000000000000000000013721503074453300230510ustar00rootroot00000000000000#ifndef GAME_MWWORLD_MANUALREF_H #define GAME_MWWORLD_MANUALREF_H #include #include "ptr.hpp" namespace MWWorld { /// \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 { // Stores the ref (LiveCellRef) by value. std::any mRef; Ptr mPtr; ManualRef(const ManualRef&); ManualRef& operator=(const ManualRef&); 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; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/movementdirection.hpp000066400000000000000000000006751503074453300246370ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/nullaction.hpp000066400000000000000000000004731503074453300232500ustar00rootroot00000000000000#ifndef GAME_MWWORLD_NULLACTION_H #define GAME_MWWORLD_NULLACTION_H #include "action.hpp" namespace MWWorld { /// \brief Action: do nothing class NullAction : public Action { void executeImp(const Ptr& actor) override {} bool isNullAction() override { return true; } }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/player.cpp000066400000000000000000000414001503074453300223620ustar00rootroot00000000000000#include "player.hpp" #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/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" namespace MWWorld { namespace { ESM::CellRef makePlayerCellRef() { ESM::CellRef result; result.blank(); result.mRefID = ESM::RefId::stringRefId("Player"); return result; } } Player::Player(const ESM::NPC* player) : mPlayer(makePlayerCellRef(), player) , mCellStore(nullptr) , mLastKnownExteriorPosition(0, 0, 0) , mMarkedPosition(ESM::Position()) , mMarkedCell(nullptr) , mTeleported(false) , mCurrentCrimeId(-1) , mPaidCrimeId(-1) , mJumping(false) { ESM::Position playerPos = mPlayer.mData.getPosition(); playerPos.pos[0] = playerPos.pos[1] = playerPos.pos[2] = 0; mPlayer.mData.setPosition(playerPos); } void Player::saveStats() { MWMechanics::NpcStats& stats = getPlayer().getClass().getNpcStats(getPlayer()); for (size_t i = 0; i < mSaveSkills.size(); ++i) mSaveSkills[i] = stats.getSkill(ESM::Skill::indexToRefId(i)).getModified(); for (size_t i = 0; i < mSaveAttributes.size(); ++i) mSaveAttributes[i] = stats.getAttribute(ESM::Attribute::indexToRefId(i)).getModified(); } void Player::restoreStats() { 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(health.getBase() / gmst.find("fWereWolfHealth")->mValue.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 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(health.getBase() * gmst.find("fWereWolfHealth")->mValue.getFloat()); for (const auto& attribute : store->get()) { MWMechanics::AttributeValue value = npcStats.getAttribute(attribute.mId); value.setModifier(attribute.mWerewolfValue - value.getModified()); npcStats.setAttribute(attribute.mId, value); } for (const auto& skill : store->get()) { // Acrobatics is set separately for some reason. if (skill.mId == ESM::Skill::Acrobatics) continue; MWMechanics::SkillValue& value = npcStats.getSkill(skill.mId); value.setModifier(skill.mWerewolfValue - value.getModified()); } } void Player::set(const ESM::NPC* player) { mPlayer.mBase = player; } void Player::setCell(MWWorld::CellStore* cellStore) { mCellStore = cellStore; } MWWorld::Ptr Player::getPlayer() { MWWorld::Ptr ptr(&mPlayer, mCellStore); return ptr; } MWWorld::ConstPtr Player::getConstPlayer() const { MWWorld::ConstPtr ptr(&mPlayer, mCellStore); return ptr; } void Player::setBirthSign(const ESM::RefId& sign) { mSign = sign; } const ESM::RefId& Player::getBirthSign() const { return mSign; } void Player::setDrawState(MWMechanics::DrawState state) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getNpcStats(ptr).setDrawState(state); } void Player::yaw(float yaw) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[2] += yaw; } void Player::pitch(float pitch) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[0] += pitch; } void Player::roll(float roll) { MWWorld::Ptr ptr = getPlayer(); ptr.getClass().getMovementSettings(ptr).mRotation[1] += roll; } MWMechanics::DrawState Player::getDrawState() { MWWorld::Ptr ptr = getPlayer(); return ptr.getClass().getNpcStats(ptr).getDrawState(); } void Player::activate() { if (MWBase::Environment::get().getWindowManager()->isGuiMode()) return; MWWorld::Ptr player = getPlayer(); const MWMechanics::NpcStats& playerStats = player.getClass().getNpcStats(player); if (playerStats.isParalyzed() || playerStats.getKnockedDown() || playerStats.isDead()) return; MWWorld::Ptr toActivate = MWBase::Environment::get().getWorld()->getFacedObject(); if (toActivate.isEmpty()) return; if (!toActivate.getClass().hasToolTip(toActivate)) return; MWBase::Environment::get().getLuaManager()->objectActivated(toActivate, player); } bool Player::wasTeleported() const { return mTeleported; } void Player::setTeleported(bool teleported) { mTeleported = teleported; } void Player::setJumping(bool jumping) { mJumping = jumping; } bool Player::getJumping() const { return mJumping; } bool Player::isInCombat() { return MWBase::Environment::get().getMechanicsManager()->getActorsFighting(getPlayer()).size() != 0; } bool Player::enemiesNearby() { return MWBase::Environment::get().getMechanicsManager()->getEnemiesNearby(getPlayer()).size() != 0; } void Player::markPosition(CellStore* markedCell, const ESM::Position& markedPosition) { mMarkedCell = markedCell; mMarkedPosition = markedPosition; } void Player::getMarkedPosition(CellStore*& markedCell, ESM::Position& markedPosition) const { markedCell = mMarkedCell; if (mMarkedCell) markedPosition = mMarkedPosition; } 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 = ESM::RefId(); mMarkedCell = nullptr; mTeleported = false; mJumping = false; mCurrentCrimeId = -1; mPaidCrimeId = -1; mPreviousItems.clear(); mLastKnownExteriorPosition = osg::Vec3f(0, 0, 0); mSaveSkills.fill(0.f); mSaveAttributes.fill(0.f); mMarkedPosition.pos[0] = 0; mMarkedPosition.pos[1] = 0; mMarkedPosition.pos[2] = 0; mMarkedPosition.rot[0] = 0; mMarkedPosition.rot[1] = 0; mMarkedPosition.rot[2] = 0; } void Player::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { ESM::Player player; mPlayer.save(player.mObject); player.mCellId = mCellStore->getCell()->getId(); player.mCurrentCrimeId = mCurrentCrimeId; player.mPaidCrimeId = mPaidCrimeId; player.mBirthsign = mSign; player.mLastKnownExteriorPosition[0] = mLastKnownExteriorPosition.x(); player.mLastKnownExteriorPosition[1] = mLastKnownExteriorPosition.y(); player.mLastKnownExteriorPosition[2] = mLastKnownExteriorPosition.z(); if (mMarkedCell) { player.mHasMark = true; player.mMarkedPosition = mMarkedPosition; player.mMarkedCell = mMarkedCell->getCell()->getId(); } else player.mHasMark = false; for (size_t i = 0; i < mSaveAttributes.size(); ++i) player.mSaveAttributes[i] = mSaveAttributes[i]; for (size_t i = 0; i < mSaveSkills.size(); ++i) player.mSaveSkills[i] = mSaveSkills[i]; player.mPreviousItems = mPreviousItems; writer.startRecord(ESM::REC_PLAY); player.save(writer); writer.endRecord(ESM::REC_PLAY); } bool Player::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_PLAY) { ESM::Player player; player.load(reader); if (!mPlayer.checkState(player.mObject)) { // this is the one object we can not silently drop. throw std::runtime_error("invalid player state record (object state)"); } if (reader.getFormatVersion() <= ESM::MaxClearModifiersFormatVersion) convertMagicEffects( player.mObject.mCreatureStats, player.mObject.mInventory, &player.mObject.mNpcStats); else if (reader.getFormatVersion() <= ESM::MaxOldCreatureStatsFormatVersion) { convertStats(player.mObject.mCreatureStats); convertEnchantmentSlots(player.mObject.mCreatureStats, player.mObject.mInventory); } else if (reader.getFormatVersion() <= ESM::MaxActiveSpellSlotIndexFormatVersion) convertEnchantmentSlots(player.mObject.mCreatureStats, player.mObject.mInventory); if (!player.mObject.mEnabled) { Log(Debug::Warning) << "Warning: Savegame attempted to disable the player."; player.mObject.mEnabled = true; } MWBase::Environment::get().getWorldModel()->deregisterLiveCellRef(mPlayer); mPlayer.load(player.mObject); for (size_t i = 0; i < mSaveAttributes.size(); ++i) mSaveAttributes[i] = player.mSaveAttributes[i]; for (size_t i = 0; i < mSaveSkills.size(); ++i) mSaveSkills[i] = player.mSaveSkills[i]; if (player.mObject.mNpcStats.mIsWerewolf) { if (reader.getFormatVersion() <= ESM::MaxOldSkillsAndAttributesFormatVersion) { setWerewolfStats(); if (player.mSetWerewolfAcrobatics) MWBase::Environment::get().getMechanicsManager()->applyWerewolfAcrobatics(getPlayer()); } } getPlayer().getClass().getCreatureStats(getPlayer()).getAiSequence().clear(); MWBase::World& world = *MWBase::Environment::get().getWorld(); 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); if (!sign) throw std::runtime_error("invalid player state record (birthsign does not exist)"); } mCurrentCrimeId = player.mCurrentCrimeId; mPaidCrimeId = player.mPaidCrimeId; mSign = player.mBirthsign; mLastKnownExteriorPosition.x() = player.mLastKnownExteriorPosition[0]; mLastKnownExteriorPosition.y() = player.mLastKnownExteriorPosition[1]; mLastKnownExteriorPosition.z() = player.mLastKnownExteriorPosition[2]; if (player.mHasMark) { if (!world.getStore().get().search(player.mMarkedCell)) player.mHasMark = false; // drop mark silently } if (player.mHasMark) { mMarkedPosition = player.mMarkedPosition; mMarkedCell = &MWBase::Environment::get().getWorldModel()->getCell(player.mMarkedCell); } else { mMarkedCell = nullptr; } mTeleported = false; mPreviousItems = player.mPreviousItems; return true; } return false; } int Player::getNewCrimeId() { return ++mCurrentCrimeId; } void Player::recordCrimeId() { mPaidCrimeId = mCurrentCrimeId; } int Player::getCrimeId() const { return mPaidCrimeId; } void Player::setPreviousItem(const ESM::RefId& boundItemId, const ESM::RefId& previousItemId) { mPreviousItems[boundItemId] = previousItemId; } ESM::RefId Player::getPreviousItem(const ESM::RefId& boundItemId) { return mPreviousItems[boundItemId]; } void Player::erasePreviousItem(const ESM::RefId& boundItemId) { mPreviousItems.erase(boundItemId); } void Player::setSelectedSpell(const ESM::RefId& spellId) { Ptr player = getPlayer(); InventoryStore& store = player.getClass().getInventoryStore(player); store.setSelectedEnchantItem(store.end()); int castChance = int(MWMechanics::getSpellSuccessChance(spellId, player)); 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))); } } openmw-openmw-0.49.0/apps/openmw/mwworld/player.hpp000066400000000000000000000072201503074453300223710ustar00rootroot00000000000000#ifndef GAME_MWWORLD_PLAYER_H #define GAME_MWWORLD_PLAYER_H #include #include #include "../mwworld/livecellref.hpp" #include "../mwmechanics/drawstate.hpp" #include #include #include #include namespace ESM { class ESMWriter; class ESMReader; } namespace Loading { class Listener; } namespace MWWorld { class CellStore; class ConstPtr; /// \brief NPC object representing the player and additional player data class Player { LiveCellRef mPlayer; MWWorld::CellStore* mCellStore; ESM::RefId mSign; osg::Vec3f mLastKnownExteriorPosition; ESM::Position mMarkedPosition; // If no position was marked, this is nullptr CellStore* mMarkedCell; bool mTeleported; 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 PreviousItems mPreviousItems; // Saved stats prior to becoming a werewolf std::array mSaveSkills; std::array mSaveAttributes; bool mJumping; public: 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; /// 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; } osg::Vec3f getLastKnownExteriorPosition() const { return mLastKnownExteriorPosition; } void set(const ESM::NPC* player); void setCell(MWWorld::CellStore* cellStore); MWWorld::Ptr getPlayer(); MWWorld::ConstPtr getConstPlayer() const; void setBirthSign(const ESM::RefId& sign); const ESM::RefId& getBirthSign() const; void setDrawState(MWMechanics::DrawState state); MWMechanics::DrawState getDrawState(); /// \todo constness /// Activate the object under the crosshair, if any void activate(); void yaw(float yaw); void pitch(float pitch); void roll(float roll); bool wasTeleported() const; void setTeleported(bool teleported); void setJumping(bool jumping); bool getJumping() const; /// 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; bool readRecord(ESM::ESMReader& reader, uint32_t type); 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 void setPreviousItem(const ESM::RefId& boundItemId, const ESM::RefId& previousItemId); ESM::RefId getPreviousItem(const ESM::RefId& boundItemId); void erasePreviousItem(const ESM::RefId& boundItemId); void setSelectedSpell(const ESM::RefId& spellId); void update(); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/positioncellgrid.hpp000066400000000000000000000004351503074453300244500ustar00rootroot00000000000000#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 openmw-openmw-0.49.0/apps/openmw/mwworld/projectilemanager.cpp000066400000000000000000000757561503074453300246060ustar00rootroot00000000000000#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 "../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 "../mwmechanics/actorutil.hpp" #include "../mwmechanics/combat.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/weapontype.hpp" #include "../mwrender/animation.hpp" #include "../mwrender/renderingmanager.hpp" #include "../mwrender/util.hpp" #include "../mwrender/vismask.hpp" #include "../mwsound/sound.hpp" #include "../mwphysics/physicssystem.hpp" #include "../mwphysics/projectile.hpp" namespace { 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().getESMStore(); const ESM::EffectList* effects; if (const ESM::Spell* spell = esmStore.get().search(id)) // check if it's a spell { sourceName = spell->mName; effects = &spell->mEffects; } else // check if it's an enchanted item { MWWorld::ManualRef ref(esmStore, id); MWWorld::Ptr ptr = ref.getPtr(); const ESM::Enchantment* ench = esmStore.get().find(ptr.getClass().getEnchantment(ptr)); sourceName = ptr.getClass().getName(ptr); effects = &ench->mEffects; } int count = 0; speed = 0.0f; ESM::EffectList projectileEffects; for (const ESM::IndexedENAMstruct& effect : effects->mList) { 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 (effect.mData.mRange != ESM::RT_Target) continue; if (magicEffect->mBolt.empty()) projectileIDs.emplace_back(ESM::RefId::stringRefId("VFX_DefaultBolt")); else projectileIDs.push_back(magicEffect->mBolt); if (!magicEffect->mBoltSound.empty()) sounds.emplace(magicEffect->mBoltSound); else 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) { 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 { 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); } return projectileEffects; } osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects) { // Calculate combined light diffuse color from magical effects osg::Vec4 lightDiffuseColor; for (const ESM::IndexedENAMstruct& enam : effects.mList) { const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get().find(enam.mData.mEffectID); lightDiffuseColor += magicEffect->getColor(); } int numberOfEffects = effects.mList.size(); lightDiffuseColor /= numberOfEffects; return lightDiffuseColor; } } namespace MWWorld { ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem, 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 SceneUtil::NodeCallback { public: RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0, -1, 0), float rotateSpeed = osg::PI * 2) : mAxis(axis) , mRotateSpeed(rotateSpeed) { } void operator()(osg::PositionAttitudeTransform* node, osg::NodeVisitor* nv) { double time = nv->getFrameStamp()->getSimulationTime(); osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis); node->setAttitude(orient); traverse(node, nv); } private: osg::Vec3f mAxis; float mRotateSpeed; }; void ProjectileManager::createModel(State& state, VFS::Path::NormalizedView 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); state.mNode->setPosition(pos); state.mNode->setAttitude(orient); osg::Group* attachTo = state.mNode; if (rotate) { 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); 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().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( Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized(weapon->mModel)), findVisitor.mFoundNode); } } if (createLight) { osg::ref_ptr projectileLight(new osg::Light); projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f)); projectileLight->setDiffuse(lightDiffuseColor); projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f)); projectileLight->setConstantAttenuation(0.f); 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); } state.mNode->addCullCallback(new SceneUtil::LightListCallback); mParent->addChild(state.mNode); state.mEffectAnimationTime = std::make_shared(); SceneUtil::AssignControllerSourcesVisitor assignVisitor(state.mEffectAnimationTime); state.mNode->accept(assignVisitor); MWRender::overrideFirstRootTexture(texture, mResourceSystem, *projectile); } void ProjectileManager::update(State& state, float duration) { state.mEffectAnimationTime->addTime(duration); } 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. pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight; } 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)); else 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 state.mActorId = -1; std::string texture; 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()) return; if (!caster.getClass().isActor() && fallbackDirection.length2() <= 0) { Log(Debug::Warning) << "Unable to launch magic bolt (direction to target is empty)"; return; } MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects); VFS::Path::Normalized model = ptr.getClass().getCorrectedModel(ptr); createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture); 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); 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 if (state.mIdMagic.size() > 1) { model = Misc::ResourceHelpers::correctMeshPath(VFS::Path::Normalized( 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(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.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().getESMStore(), projectile.getCellRef().getRefId()); MWWorld::Ptr ptr = ref.getPtr(); const VFS::Path::Normalized 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); state.mToDelete = false; mProjectiles.push_back(state); } void ProjectileManager::updateCasters() { for (auto& state : mProjectiles) mPhysics->setCaster(state.mProjectileId, state.getCaster()); 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. // TODO: should object-type caster be restored from savegame? if (state.mActorId == -1) continue; auto caster = state.getCaster(); if (caster.isEmpty()) { Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId; cleanupMagicBolt(state); continue; } mPhysics->setCaster(state.mProjectileId, caster); } } void ProjectileManager::update(float dt) { periodicCleanup(dt); moveProjectiles(dt); moveMagicBolts(dt); } void ProjectileManager::periodicCleanup(float dt) { mCleanupTimer -= dt; if (mCleanupTimer <= 0.0f) { mCleanupTimer = 2.0f; 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; }; for (auto& projectileState : mProjectiles) { if (isCleanable(projectileState)) cleanupProjectile(projectileState); } for (auto& magicBoltState : mMagicBolts) { if (isCleanable(magicBoltState)) cleanupMagicBolt(magicBoltState); } } } void ProjectileManager::moveMagicBolts(float duration) { const bool normaliseRaceSpeed = Settings::game().mNormaliseRaceSpeed; for (auto& magicBoltState : mMagicBolts) { if (magicBoltState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); if (!projectile->isActive()) continue; // If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame. MWWorld::Ptr caster = magicBoltState.getCaster(); if (!caster.isEmpty() && caster.getClass().isActor()) { 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 = store.get().find("fTargetSpellMaxSpeed")->mValue.getFloat(); float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed; if (!normaliseRaceSpeed && !caster.isEmpty() && caster.getClass().isNpc()) { const auto npc = caster.get()->mBase; const auto race = store.get().find(npc->mRace); speed *= npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight; } osg::Vec3f direction = orient * osg::Vec3f(0, 1, 0); direction.normalize(); 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. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); } } void ProjectileManager::moveProjectiles(float duration) { for (auto& projectileState : mProjectiles) { if (projectileState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); if (!projectile->isActive()) 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; projectile->setVelocity(projectileState.mVelocity); // 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); projectileState.mNode->setAttitude(orient); } update(projectileState, duration); 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. std::vector targetActors; if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer()) caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors); projectile->setValidTargets(targetActors); } } void ProjectileManager::processHits() { for (auto& projectileState : mProjectiles) { if (projectileState.mToDelete) continue; auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId); const auto pos = projectile->getSimulationPosition(); projectileState.mNode->setPosition(pos); if (projectile->isActive()) continue; const auto target = projectile->getTarget(); auto caster = projectileState.getCaster(); assert(target != caster); if (caster.isEmpty()) caster = target; // Try to get a Ptr to the bow that was used. It might no longer exist. 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() && invIt->getCellRef().getRefId() == projectileState.mBowId) bow = *invIt; } 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) continue; auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId); const auto pos = projectile->getSimulationPosition(); magicBoltState.mNode->setPosition(pos); for (const auto& sound : magicBoltState.mSounds) sound->setPosition(pos); if (projectile->isActive()) continue; const auto target = projectile->getTarget(); const auto caster = magicBoltState.getCaster(); assert(target != caster); MWMechanics::CastSpell cast(caster, target); cast.mHitPosition = Misc::Convert::toOsg(projectile->getHitPosition()); cast.mId = magicBoltState.mSpellId; cast.mSourceName = magicBoltState.mSourceName; 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); } 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()); } void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state) { mParent->removeChild(state.mNode); mPhysics->removeProjectile(state.mProjectileId); state.mToDelete = true; } void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state) { mParent->removeChild(state.mNode); mPhysics->removeProjectile(state.mProjectileId); state.mToDelete = true; for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++) { MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter)); } } void ProjectileManager::clear() { for (auto& mProjectile : mProjectiles) cleanupProjectile(mProjectile); mProjectiles.clear(); for (auto& mMagicBolt : mMagicBolts) cleanupMagicBolt(mMagicBolt); mMagicBolts.clear(); } void ProjectileManager::write(ESM::ESMWriter& writer, Loading::Listener& progress) const { for (std::vector::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it) { writer.startRecord(ESM::REC_PROJ); ESM::ProjectileState state; state.mId = it->mIdArrow; state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mActorId = it->mActorId; state.mBowId = it->mBowId; state.mVelocity = it->mVelocity; state.mAttackStrength = it->mAttackStrength; state.save(writer); writer.endRecord(ESM::REC_PROJ); } for (std::vector::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) { writer.startRecord(ESM::REC_MPRJ); ESM::MagicBoltState state; state.mId = it->mIdMagic.at(0); 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; state.save(writer); writer.endRecord(ESM::REC_MPRJ); } } bool ProjectileManager::readRecord(ESM::ESMReader& reader, uint32_t type) { if (type == ESM::REC_PROJ) { ESM::ProjectileState esm; esm.load(reader); ProjectileState state; state.mActorId = esm.mActorId; state.mBowId = esm.mBowId; state.mVelocity = esm.mVelocity; state.mIdArrow = esm.mId; state.mAttackStrength = esm.mAttackStrength; state.mToDelete = false; VFS::Path::Normalized model; try { MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), esm.mId); MWWorld::Ptr ptr = ref.getPtr(); 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); } 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)); mProjectiles.push_back(state); return true; } if (type == ESM::REC_MPRJ) { ESM::MagicBoltState esm; esm.load(reader); MagicBoltState state; state.mIdMagic.push_back(esm.mId); 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); } catch (const std::exception& e) { Log(Debug::Warning) << "Failed to recreate magic projectile for " << esm.mId << " and spell " << state.mSpellId << " while reading projectile record: " << e.what(); return true; } state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as // projectile effects, so we can't calculate it from the save // file's effect list, which is already trimmed of non-projectile // effects. We need to use the stored value. VFS::Path::Normalized model; try { MWWorld::ManualRef ref(*MWBase::Environment::get().getESMStore(), state.mIdMagic.at(0)); MWWorld::Ptr ptr = ref.getPtr(); model = ptr.getClass().getCorrectedModel(ptr); } 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); 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); if (sound) state.mSounds.push_back(sound); } mMagicBolts.push_back(state); return true; } return false; } int ProjectileManager::countSavedGameRecords() const { return mMagicBolts.size() + mProjectiles.size(); } MWWorld::Ptr ProjectileManager::State::getCaster() { if (!mCasterHandle.isEmpty()) return mCasterHandle; return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId); } } openmw-openmw-0.49.0/apps/openmw/mwworld/projectilemanager.hpp000066400000000000000000000100661503074453300245720ustar00rootroot00000000000000#ifndef OPENMW_MWWORLD_PROJECTILEMANAGER_H #define OPENMW_MWWORLD_PROJECTILEMANAGER_H #include #include #include #include #include #include "../mwbase/soundmanager.hpp" #include "ptr.hpp" namespace MWPhysics { class PhysicsSystem; } namespace Loading { class Listener; } namespace osg { class Group; class Quat; } namespace Resource { class ResourceSystem; } namespace MWRender { class EffectAnimationTime; class RenderingManager; } namespace MWWorld { class ProjectileManager { public: 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 ESM::RefId& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection, ESM::RefNum item); 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(); void update(float dt); void processHits(); /// 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); int countSavedGameRecords() const; private: osg::ref_ptr mParent; Resource::ResourceSystem* mResourceSystem; MWRender::RenderingManager* mRendering; MWPhysics::PhysicsSystem* mPhysics; float mCleanupTimer; struct State { osg::ref_ptr mNode; std::shared_ptr mEffectAnimationTime; int mActorId; int mProjectileId; // TODO: this will break when the game is saved and reloaded, since there is currently // no way to write identifiers for non-actors to a savegame. MWWorld::Ptr mCasterHandle; MWWorld::Ptr getCaster(); // MW-ids of a magic projectile std::vector mIdMagic; // MW-id of an arrow projectile ESM::RefId mIdArrow; bool mToDelete; }; struct MagicBoltState : public State { ESM::RefId mSpellId; // Name of item to display as effect source in magic menu (in case we casted an enchantment) std::string mSourceName; ESM::EffectList mEffects; float mSpeed; // Refnum of the casting item ESM::RefNum mItem; std::vector mSounds; 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) ESM::RefId mBowId; osg::Vec3f mVelocity; float mAttackStrength; bool mThrown; }; std::vector mMagicBolts; std::vector mProjectiles; void cleanupProjectile(ProjectileState& state); void cleanupMagicBolt(MagicBoltState& state); void periodicCleanup(float dt); void moveProjectiles(float dt); void moveMagicBolts(float dt); void createModel(State& state, VFS::Path::NormalizedView 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&); }; } #endif openmw-openmw-0.49.0/apps/openmw/mwworld/ptr.cpp000066400000000000000000000015261503074453300217000ustar00rootroot00000000000000#include "ptr.hpp" #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(); } } } openmw-openmw-0.49.0/apps/openmw/mwworld/ptr.hpp000066400000000000000000000135241503074453300217060ustar00rootroot00000000000000#ifndef GAME_MWWORLD_PTR_H #define GAME_MWWORLD_PTR_H #include #include #include #include #include "livecellref.hpp" namespace MWWorld { class ContainerStore; class CellStore; struct LiveCellRefBase; /// \brief Pointer to a LiveCellRef /// @note PtrBase is never used directly and needed only to define Ptr and ConstPtr template