1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-02-28 22:09:42 +00:00

Merge branch openmw:master into lua_case

This commit is contained in:
Zackhasacat 2024-12-05 01:36:32 +00:00
commit 5bcf344795
2180 changed files with 100926 additions and 36367 deletions

View file

@ -1,14 +1,12 @@
name: CMake
on:
push:
branches:
- 'master'
pull_request:
branches: [ master ]
- push
- pull_request
env:
BUILD_TYPE: RelWithDebInfo
VCPKG_DEPS_TAG: 2024-11-10
jobs:
Ubuntu:
@ -30,13 +28,27 @@ jobs:
max-size: 1000M
- name: Configure
run: cmake . -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=1 -DUSE_SYSTEM_TINYXML=1 -DBUILD_UNITTESTS=1 -DCMAKE_INSTALL_PREFIX=install
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: make -j3
run: cmake --build . -- -j$(nproc)
- name: Test
run: ./openmw_test_suite
- 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
@ -71,10 +83,182 @@ jobs:
max-size: 1000M
- name: Configure
run: |
rm -fr build # remove the build directory
CI/before_script.osx.sh
run: CI/before_script.osx.sh
- name: Build
run: |
cd build
make -j $(sysctl -n hw.logicalcpu) package
Windows:
strategy:
fail-fast: true
matrix:
image:
- "2019"
- "2022"
name: windows-${{ matrix.image }}
runs-on: windows-${{ matrix.image }}
env:
archive: FAILEDTODOWNLOAD
steps:
- uses: actions/checkout@v2
- name: Create directories for dependencies
run: |
mkdir -p ${{ github.workspace }}/deps
mkdir -p ${{ github.workspace }}/deps/Qt
- name: Download prebuilt vcpkg packages
working-directory: ${{ github.workspace }}/deps
run: |
$MANIFEST = "vcpkg-x64-${{ matrix.image }}-${{ env.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-${{ matrix.image }}-${{ env.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=RelWithDebInfo
-D CMAKE_TOOLCHAIN_FILE='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/scripts/buildsystems/vcpkg.cmake'
-D CMAKE_PREFIX_PATH='${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64'
-D LuaJit_INCLUDE_DIR='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/include/luajit'
-D LuaJit_LIBRARY='${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/lib/lua51.lib'
-D BUILD_BENCHMARKS=ON
-D BUILD_COMPONENTS_TESTS=ON
-D BUILD_OPENMW_TESTS=ON
-D BUILD_OPENCS_TESTS=ON
-D OPENMW_USE_SYSTEM_SQLITE3=OFF
-D OPENMW_USE_SYSTEM_YAML_CPP=OFF
-D OPENMW_LTO_BUILD=ON
- name: Build OpenMW
run: cmake --build ${{ github.workspace }}/build
- name: Install OpenMW
run: cmake --install ${{ github.workspace }}/build --prefix ${{ github.workspace }}/install
- name: Copy missing DLLs
run: |
cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/Release/MyGUIEngine.dll ${{ github.workspace }}/install
cp -Filter *.dll -Recurse ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/osgPlugins-3.6.5 ${{ github.workspace }}/install
cp ${{ github.workspace }}/deps/vcpkg-x64-${{ matrix.image }}-${{ env.VCPKG_DEPS_TAG }}/installed/x64-windows/bin/*.dll ${{ github.workspace }}/install
- name: Copy Qt DLLs
working-directory: ${{ github.workspace }}/deps/Qt/6.6.3/msvc2019_64
run: |
cp bin/Qt6Core.dll ${{ github.workspace }}/install
cp bin/Qt6Gui.dll ${{ github.workspace }}/install
cp bin/Qt6Network.dll ${{ github.workspace }}/install
cp bin/Qt6OpenGL.dll ${{ github.workspace }}/install
cp bin/Qt6OpenGLWidgets.dll ${{ github.workspace }}/install
cp bin/Qt6Widgets.dll ${{ github.workspace }}/install
cp bin/Qt6Svg.dll ${{ github.workspace }}/install
mkdir ${{ github.workspace }}/install/styles
cp plugins/styles/qwindowsvistastyle.dll ${{ github.workspace }}/install/styles
mkdir ${{ github.workspace }}/install/platforms
cp plugins/platforms/qwindows.dll ${{ github.workspace }}/install/platforms
mkdir ${{ github.workspace }}/install/imageformats
cp plugins/imageformats/qsvg.dll ${{ github.workspace }}/install/imageformats
mkdir ${{ github.workspace }}/install/iconengines
cp plugins/iconengines/qsvgicon.dll ${{ github.workspace }}/install/iconengines
- name: Move pdb files
run: |
robocopy install pdb *.pdb /MOVE
if ($lastexitcode -lt 8) {
$global:LASTEXITCODE = $null
}
- name: Remove extra pdb files
shell: bash
run: |
rm -rf install/bin
rm -rf install/_deps
- name: Generate CI-ID.txt
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
job_url=$(gh run --repo ${{ github.repository }} view ${{ github.run_id }} --json jobs --jq '.jobs[] | select(.name == "windows-${{ matrix.image }}") | .url')
printf "Ref ${{ github.ref }}\nJob ${job_url}\nCommit ${{ github.sha }}\n" > install/CI-ID.txt
cp install/CI-ID.txt pdb/CI-ID.txt
- name: Store OpenMW archived pdb files
uses: actions/upload-artifact@v4
with:
name: openmw-windows-${{ matrix.image }}-pdb-${{ github.sha }}
path: ${{ github.workspace }}/pdb/*
- name: Store OpenMW build artifacts
uses: actions/upload-artifact@v4
with:
name: openmw-windows-${{ matrix.image }}-${{ github.sha }}
path: ${{ github.workspace }}/install/*
- name: Add install directory to PATH
shell: bash
run: echo '${{ github.workspace }}/install' >> ${GITHUB_PATH}
- name: Run components tests
run: build/components-tests.exe
- name: Run OpenMW tests
run: build/openmw-tests.exe
- name: Run OpenMW-CS tests
run: build/openmw-cs-tests.exe
- name: Run detournavigator navmeshtilescache benchmark
run: build/openmw_detournavigator_navmeshtilescache_benchmark.exe
- name: Run settings access benchmark
run: build/openmw_settings_access_benchmark.exe
- name: Run esm refid benchmark
run: build/openmw_esm_refid_benchmark.exe

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ Doxygen
.idea
cmake-build-*
files/windows/*.aps
.cache/clangd
## qt-creator
CMakeLists.txt.user*
.vs

View file

@ -22,11 +22,11 @@ variables:
# These can be specified per job or per pipeline
ARTIFACT_COMPRESSION_LEVEL: "fast"
CACHE_COMPRESSION_LEVEL: "fast"
FF_TIMESTAMPS: "true"
.Ubuntu_Image:
tags:
- docker
- linux
- saas-linux-medium-amd64
image: ubuntu:22.04
rules:
- if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"
@ -70,15 +70,16 @@ Ubuntu_GCC_preprocess:
- df -h
- du -sh .
- cmake --install .
- if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite --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}" ]]; 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 '^openmw_tests.xml$' -e '^openmw_cs_tests.xml$' | xargs -I '{}' rm -rf './{}'
- 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/
@ -91,8 +92,7 @@ Ubuntu_GCC_preprocess:
Coverity:
tags:
- docker
- linux
- saas-linux-medium-amd64
image: ubuntu:22.04
stage: build
rules:
@ -116,6 +116,7 @@ Coverity:
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
@ -142,7 +143,7 @@ Ubuntu_GCC:
variables:
CC: gcc
CXX: g++
CCACHE_SIZE: 4G
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
@ -172,6 +173,20 @@ Clang_Format:
- 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
@ -181,8 +196,9 @@ Teal:
script:
- CI/teal_ci.sh
artifacts:
when: always
paths:
- teal_declarations.zip
- teal_declarations
Ubuntu_GCC_Debug:
extends: .Ubuntu
@ -193,9 +209,10 @@ Ubuntu_GCC_Debug:
variables:
CC: gcc
CXX: g++
CCACHE_SIZE: 4G
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
@ -408,7 +425,7 @@ Ubuntu_Clang_Tidy_other:
needs:
- Ubuntu_Clang_Tidy_components
variables:
BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest openmw_test_suite openmw-navmeshtool openmw-bulletobjecttool
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:
@ -445,6 +462,7 @@ Ubuntu_Clang_tests_Debug:
stage: test
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
EXAMPLE_SUITE_REVISION: f51b832e033429a7cdc520e0e48d7dfdb9141caa
cache:
paths:
- .cache/pip
@ -484,7 +502,6 @@ Ubuntu_GCC_integration_tests_asan:
paths:
- ccache/
script:
- rm -fr build # remove the build directory
- CI/before_install.osx.sh
- export CCACHE_BASEDIR="$(pwd)"
- export CCACHE_DIR="$(pwd)/ccache"
@ -492,7 +509,7 @@ Ubuntu_GCC_integration_tests_asan:
- ccache -z -M "${CCACHE_SIZE}"
- CI/before_script.osx.sh
- cd build; make -j $(sysctl -n hw.logicalcpu) package
- for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}_${CI_JOB_ID}.dmg"; done
- for dmg in *.dmg; do mv "$dmg" "${dmg%.dmg}_${CI_COMMIT_REF_NAME##*/}.dmg"; done
- |
if [[ -n "${AWS_ACCESS_KEY_ID}" ]]; then
artifactDirectory="${CI_PROJECT_NAMESPACE//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_REF_NAME//[\"<>|$'\t'\/\\?*]/_}/${CI_COMMIT_SHORT_SHA//[\"<>|$'\t'\/\\?*]/_}-${CI_JOB_ID//[\"<>|$'\t'\/\\?*]/_}/"
@ -504,21 +521,59 @@ Ubuntu_GCC_integration_tests_asan:
artifacts:
paths:
- build/OpenMW-*.dmg
- "build/**/*.log"
macOS13_Xcode14_arm64:
macOS14_Xcode15_arm64:
extends: .MacOS
image: macos-12-xcode-14
image: macos-14-xcode-15
tags:
- saas-macos-medium-m1
cache:
key: macOS12_Xcode14_arm64.v4
key: macOS14_Xcode15_arm64.v1
variables:
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.zip" -o awscli-exe-linux-x86_64.zip
- unzip -d awscli-exe-linux-x86_64 awscli-exe-linux-x86_64.zip
- pushd awscli-exe-linux-x86_64
- ./aws/install
- popd
- aws --version
- unzip -d sym_store *sym_store.zip
- shopt -s globstar
- |
for file in sym_store/**/*.exe; do
if [[ -f "$file" ]]; then
gcab --create --zip --nopath "${file%.exe}.ex_" "$file"
fi
done
- |
for file in sym_store/**/*.dll; do
if [[ -f "$file" ]]; then
gcab --create --zip --nopath "${file%.dll}.dl_" "$file"
fi
done
- |
for file in sym_store/**/*.pdb; do
if [[ -f "$file" ]]; then
gcab --create --zip --nopath "${file%.pdb}.pd_" "$file"
fi
done
- |
if [[ -v AWS_ACCESS_KEY_ID ]]; then
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude '*' --include '*.ex_' --include '*.dl_' --include '*.pd_' sym_store s3://openmw-sym
fi
.Windows_Ninja_Base:
tags:
- windows
- saas-windows-medium-amd64
rules:
- if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"
before_script:
@ -555,60 +610,53 @@ macOS13_Xcode14_arm64:
- $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 MSVC2019_64_Ninja\.cmake\api\v1\query\codemodel-v2
- sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -N -b -t -C $multiview -E
- 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 MSVC2019_64_Ninja
- cd MSVC2022_64_Ninja
- .\ActivateMSVC.ps1
- cmake --build . --config $config
- ccache --show-stats
- cmake --build . --config $config --target $targets
- ccache --show-stats -v
- cd $config
- echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt
- $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/"
- Get-ChildItem -Recurse *.ilk | Remove-Item
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
}
Push-Location ..
..\CI\Store-Symbols.ps1
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym
}
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
..\CI\Store-Symbols.ps1 -SkipCompress
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
Pop-Location
Get-ChildItem -Recurse *.pdb | Remove-Item
}
- 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*'
- 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*'
- |
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
}
- if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } }
after_script:
- Get-Volume
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:
key: ninja-v7
key: ninja-2022-v11
paths:
- ccache
- deps
- MSVC2019_64_Ninja/deps/Qt
- MSVC2022_64_Ninja/deps/Qt
artifacts:
when: always
paths:
- "*.zip"
- "*.log"
- MSVC2019_64_Ninja/*.log
- MSVC2019_64_Ninja/*/*.log
- MSVC2019_64_Ninja/*/*/*.log
- MSVC2019_64_Ninja/*/*/*/*.log
- MSVC2019_64_Ninja/*/*/*/*/*.log
- MSVC2019_64_Ninja/*/*/*/*/*/*.log
- MSVC2019_64_Ninja/*/*/*/*/*/*/*.log
- MSVC2019_64_Ninja/*/*/*/*/*/*/*/*.log
- 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
@ -618,6 +666,13 @@ macOS13_Xcode14_arm64:
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
@ -625,23 +680,53 @@ macOS13_Xcode14_arm64:
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: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
# 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:
- windows
- saas-windows-medium-amd64
rules:
- if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"
before_script:
@ -651,7 +736,6 @@ macOS13_Xcode14_arm64:
- 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 python -y
- choco install awscli -y
@ -674,62 +758,50 @@ macOS13_Xcode14_arm64:
- $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 MSVC2019_64\.cmake\api\v1\query\codemodel-v2
- sh CI/before_script.msvc.sh -c $config -p Win64 -v 2019 -k -V -b -t -C $multiview -E
- cd MSVC2019_64
- 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
- ccache --show-stats
- cmake --build . --config $config --target $targets
- cd $config
- echo "CI_COMMIT_REF_NAME ${CI_COMMIT_REF_NAME}`nCI_JOB_ID ${CI_JOB_ID}`nCI_COMMIT_SHA ${CI_COMMIT_SHA}" | Out-File -Encoding UTF8 CI-ID.txt
- $artifactDirectory = "$(Make-SafeFileName("${CI_PROJECT_NAMESPACE}"))/$(Make-SafeFileName("${CI_COMMIT_REF_NAME}"))/$(Make-SafeFileName("${CI_COMMIT_SHORT_SHA}-${CI_JOB_ID}"))/"
- Get-ChildItem -Recurse *.ilk | Remove-Item
- |
if (Get-ChildItem -Recurse *.pdb) {
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" '*.pdb' CI-ID.txt
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_symbols.zip"))" s3://openmw-artifacts/${artifactDirectory}
}
Push-Location ..
..\CI\Store-Symbols.ps1
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp --recursive --exclude * --include *.ex_ --include *.dl_ --include *.pd_ .\SymStore s3://openmw-sym
}
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
..\CI\Store-Symbols.ps1 -SkipCompress
7z a -tzip "..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}_${CI_JOB_ID}_sym_store.zip"))" '.\SymStore\*' $config\CI-ID.txt
Pop-Location
Get-ChildItem -Recurse *.pdb | Remove-Item
}
- 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*'
- 7z a -tzip "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" '*'
- |
if (Test-Path env:AWS_ACCESS_KEY_ID) {
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2019_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
aws --endpoint-url https://rgw.ctrl-c.liu.se s3 cp "..\..\$(Make-SafeFileName("OpenMW_MSVC2022_64_${config}_${CI_COMMIT_REF_NAME}.zip"))" s3://openmw-artifacts/${artifactDirectory}
}
- if ($executables) { foreach ($exe in $executables.Split(',')) { & .\$exe } }
after_script:
- Get-Volume
- Copy-Item C:\ProgramData\chocolatey\logs\chocolatey.log
cache:
key: msbuild-v7
key: msbuild-2022-v11
paths:
- ccache
- deps
- MSVC2019_64/deps/Qt
- MSVC2022_64/deps/Qt
artifacts:
when: always
paths:
- "*.zip"
- "*.log"
- MSVC2019_64/*.log
- MSVC2019_64/*/*.log
- MSVC2019_64/*/*/*.log
- MSVC2019_64/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*/*/*.log
- MSVC2019_64/*/*/*/*/*/*/*/*.log
- 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
@ -739,27 +811,61 @@ macOS13_Xcode14_arm64:
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: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
# 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:
- linux
- saas-linux-medium-amd64
image: psi29a/android-ndk:focal-ndk22
rules:
- if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "merge_request_event"

View file

@ -4,7 +4,10 @@ sphinx:
configuration: docs/source/conf.py
python:
version: 3.8
install:
- requirements: docs/requirements.txt
build:
os: ubuntu-22.04
tools:
python: "3.8"

View file

@ -15,6 +15,7 @@ Programmers
Nicolay Korslund - Project leader 2008-2010
scrawl - Top contributor
AbduSharif
Adam Hogan (aurix)
Aesylwinn
aegis
@ -60,6 +61,7 @@ Programmers
Cory F. Cohen (cfcohen)
Cris Mihalache (Mirceam)
crussell187
Sam Hellawell (cykoder)
Dan Vukelich (sanchezman)
darkf
Dave Corley (S3ctor)
@ -79,6 +81,7 @@ Programmers
Eduard Cot (trombonecot)
Eli2
Emanuel Guével (potatoesmaster)
Epoch
Eris Caffee (eris)
eroen
escondida
@ -96,6 +99,7 @@ Programmers
gugus/gus
guidoj
Haoda Wang (h313)
holorat
hristoast
Internecine
Ivan Beloborodov (myrix)
@ -140,6 +144,8 @@ Programmers
Lordrea
Łukasz Gołębiewski (lukago)
Lukasz Gromanowski (lgro)
Mads Sandvei (Foal)
Maksim Eremenko (Max Yari)
Marc Bouvier (CramitDeFrog)
Marcin Hulist (Gohan)
Mark Siewert (mark76)
@ -150,6 +156,7 @@ Programmers
Mateusz Malisz (malice)
Max Henzerling (SaintMercury)
megaton
Mehdi Yousfi-Monod (mym)
Michael Hogan (Xethik)
Michael Mc Donnell
Michael Papageorgiou (werdanith)
@ -186,6 +193,7 @@ Programmers
pkubik
PLkolek
PlutonicOverkill
Qlonever
Radu-Marius Popovici (rpopovici)
Rafael Moura (dhustkoder)
Randy Davin (Kindi)
@ -225,6 +233,7 @@ Programmers
thegriglat
Thomas Luppi (Digmaster)
tlmullis
trav
tri4ng1e
Thoronador
Tobias Tribble (zackhasacat)
@ -244,6 +253,7 @@ Programmers
xyzz
Yohaulticetl
Yuri Krupenin
Yury Stepovikov
zelurker
Documentation

View file

@ -2,80 +2,247 @@
------
Bug #2623: Snowy Granius doesn't prioritize conjuration spells
Bug #3438: NPCs can't hit bull netch with melee weapons
Bug #3842: Body part skeletons override the main skeleton
Bug #4127: Weapon animation looks choppy
Bug #4204: Dead slaughterfish doesn't float to water surface after loading saved game
Bug #4207: RestoreHealth/Fatigue spells have a huge priority even if a success chance is near 0
Bug #4382: Sound output device does not change when it should
Bug #4508: Can't stack enchantment buffs from different instances of the same self-cast generic magic apparel
Bug #4610: Casting a Bound Weapon spell cancels the casting animation by equipping the weapon prematurely
Bug #4683: Disposition decrease when player commits crime is not implemented properly
Bug #4742: Actors with wander never stop walking after Loopgroup Walkforward
Bug #4743: PlayGroup doesn't play non-looping animations correctly
Bug #4754: Stack of ammunition cannot be equipped partially
Bug #4816: GetWeaponDrawn returns 1 before weapon is attached
Bug #4822: Non-weapon equipment and body parts can't inherit time from parent animation
Bug #4898: Odd/Incorrect lighting on meshes
Bug #5057: Weapon swing sound plays at same pitch whether it hits or misses
Bug #5062: Root bone rotations for NPC animation don't work the same as for creature animation
Bug #5065: Actors with scripted animation still try to wander and turn around without moving
Bug #5066: Quirks with starting and stopping scripted animations
Bug #5129: Stuttering animation on Centurion Archer
Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place
Bug #5371: Keyframe animation tracks are used for any file that begins with an X
Bug #5413: Enemies do a battlecry everytime the player summons a creature
Bug #5714: Touch spells cast using ExplodeSpell don't always explode
Bug #5755: Reset friendly hit counter
Bug #5849: Paralysis breaks landing
Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla
Bug #5883: Immobile creatures don't cause water ripples
Bug #5977: Fatigueless NPCs' corpse underwater changes animation on game load
Bug #6025: Subrecords cannot overlap records
Bug #6027: Collisionshape becomes spiderweb-like when the mesh is too complex
Bug #6097: Level Progress Tooltip Sometimes Not Updated
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 #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
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
@ -84,21 +251,75 @@
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 #7284: Searching in the console with regex and toggleable case-sensitivity
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
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
------
@ -312,7 +533,6 @@
Feature #6700: Support windowed fullscreen
Feature #6706: Save the size of the Options window
Feature #6721: OpenMW-CS: Add option to open records in new window
Feature #6823: Animation layering for osgAnimation formats
Feature #6867: Add a way to localize hardcoded strings in GUI
Feature #6888: Add switch for armor degradation fix
Feature #6925: Allow to use a mouse wheel to rotate a head in the race selection menu

View file

@ -1,12 +1,23 @@
param (
[switch] $SkipCompress
)
$ErrorActionPreference = "Stop"
if (-Not (Test-Path CMakeCache.txt))
{
Write-Error "This script must be run from the build directory."
}
if (-Not (Test-Path .cmake\api\v1\reply))
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
@ -43,10 +54,31 @@ finally
if (-not (Test-Path symstore-venv))
{
python -m venv symstore-venv
if ($LASTEXITCODE -ne 0) {
Write-Error "Command exited with code $LASTEXITCODE"
}
}
if (-not (Test-Path symstore-venv\Scripts\symstore.exe))
$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==0.3.3
symstore-venv\Scripts\pip install symstore==$symstoreVersion
if ($LASTEXITCODE -ne 0) {
Write-Error "Command exited with code $LASTEXITCODE"
}
}
$artifacts = $artifacts | Where-Object { Test-Path $_ }
symstore-venv\Scripts\symstore --compress .\SymStore @artifacts
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."

View file

@ -1,4 +0,0 @@
#!/bin/bash -ex
#sudo ln -sf /usr/bin/clang-6 /usr/local/bin/clang
#sudo ln -sf /usr/bin/clang++-6 /usr/local/bin/clang++

View file

@ -1,41 +1,30 @@
#!/bin/sh -ex
export HOMEBREW_NO_EMOJI=1
brew uninstall --ignore-dependencies python@3.8 || true
brew uninstall --ignore-dependencies python@3.9 || true
brew uninstall --ignore-dependencies qt@6 || true
brew uninstall --ignore-dependencies jpeg || true
export HOMEBREW_NO_INSTALL_CLEANUP=1
export HOMEBREW_AUTOREMOVE=1
brew tap --repair
brew update --quiet
# Some of these tools can come from places other than brew, so check before installing
brew reinstall xquartz fontconfig freetype harfbuzz brotli
# Fix: can't open file: @loader_path/libbrotlicommon.1.dylib (No such file or directory)
BREW_LIB_PATH="$(brew --prefix)/lib"
install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlidec.1.dylib
install_name_tool -change "@loader_path/libbrotlicommon.1.dylib" "${BREW_LIB_PATH}/libbrotlicommon.1.dylib" ${BREW_LIB_PATH}/libbrotlienc.1.dylib
brew install curl xquartz gd fontconfig freetype harfbuzz brotli
command -v ccache >/dev/null 2>&1 || brew install ccache
command -v cmake >/dev/null 2>&1 || brew install cmake
command -v qmake >/dev/null 2>&1 || brew install qt@5
export PATH="/opt/homebrew/opt/qt@5/bin:$PATH"
# Install deps
brew install icu4c yaml-cpp sqlite
brew install openal-soft icu4c yaml-cpp sqlite
ccache --version
cmake --version
qmake --version
if [[ "${MACOS_AMD64}" ]]; then
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20221113.zip -o ~/openmw-deps.zip
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240802.zip -o ~/openmw-deps.zip
unzip -o ~/openmw-deps.zip -d /tmp > /dev/null
else
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20230722_arm64.zip -o ~/openmw-deps.zip
curl -fSL -R -J https://gitlab.com/OpenMW/openmw-deps/-/raw/main/macos/openmw-deps-20240818-arm64.tar.xz -o ~/openmw-deps.tar.xz
tar xf ~/openmw-deps.tar.xz -C /tmp > /dev/null
fi
unzip -o ~/openmw-deps.zip -d /tmp > /dev/null

View file

@ -7,14 +7,6 @@ free -m
# Silence a git warning
git config --global advice.detachedHead false
BUILD_UNITTESTS=OFF
BUILD_BENCHMARKS=OFF
if [[ "${BUILD_TESTS_ONLY}" ]]; then
BUILD_UNITTESTS=ON
BUILD_BENCHMARKS=ON
fi
# setup our basic cmake build options
declare -a CMAKE_CONF_OPTS=(
-DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}"
@ -22,7 +14,7 @@ declare -a CMAKE_CONF_OPTS=(
-DCMAKE_C_COMPILER_LAUNCHER=ccache
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache
-DCMAKE_INSTALL_PREFIX=install
-DBUILD_SHARED_LIBS=OFF
-DBUILD_SHARED_LIBS="${BUILD_SHARED_LIBS:-OFF}"
-DUSE_SYSTEM_TINYXML=ON
-DOPENMW_USE_SYSTEM_RECASTNAVIGATION=ON
-DOPENMW_CXX_FLAGS="-Werror -Werror=implicit-fallthrough" # flags specific to OpenMW project
@ -47,13 +39,13 @@ fi
if [[ $CI_CLANG_TIDY ]]; then
CMAKE_CONF_OPTS+=(
-DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*"
-DBUILD_UNITTESTS=ON
-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}
@ -103,9 +95,10 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then
-DBUILD_NAVMESHTOOL=OFF \
-DBUILD_BULLETOBJECTTOOL=OFF \
-DBUILD_NIFTEST=OFF \
-DBUILD_UNITTESTS=${BUILD_UNITTESTS} \
-DBUILD_OPENCS_TESTS=${BUILD_UNITTESTS} \
-DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \
-DBUILD_COMPONENTS_TESTS=ON \
-DBUILD_OPENMW_TESTS=ON \
-DBUILD_OPENCS_TESTS=ON \
-DBUILD_BENCHMARKS=ON \
..
elif [[ "${BUILD_OPENMW_ONLY}" ]]; then
${ANALYZE} cmake \

View file

@ -14,16 +14,6 @@ MISSINGTOOLS=0
command -v 7z >/dev/null 2>&1 || { echo "Error: 7z (7zip) is not on the path."; MISSINGTOOLS=1; }
command -v cmake >/dev/null 2>&1 || { echo "Error: cmake (CMake) is not on the path."; MISSINGTOOLS=1; }
MISSINGPYTHON=0
if ! command -v python >/dev/null 2>&1; then
echo "Warning: Python is not on the path, automatic Qt installation impossible."
MISSINGPYTHON=1
elif ! python --version >/dev/null 2>&1; then
echo "Warning: Python is (probably) fake stub Python that comes bundled with newer versions of Windows, automatic Qt installation impossible."
echo "If you think you have Python installed, try changing the order of your PATH environment variable in Advanced System Settings."
MISSINGPYTHON=1
fi
if [ $MISSINGTOOLS -ne 0 ]; then
wrappedExit 1
fi
@ -54,7 +44,6 @@ function unixPathAsWindows {
fi
}
APPVEYOR=${APPVEYOR:-}
CI=${CI:-}
STEP=${STEP:-}
@ -72,10 +61,8 @@ PDBS=""
PLATFORM=""
CONFIGURATIONS=()
TEST_FRAMEWORK=""
GOOGLE_INSTALL_ROOT=""
INSTALL_PREFIX="."
BUILD_BENCHMARKS=""
OSG_MULTIVIEW_BUILD=""
USE_WERROR=""
USE_CLANG_TIDY=""
@ -144,9 +131,6 @@ while [ $# -gt 0 ]; do
b )
BUILD_BENCHMARKS=true ;;
M )
OSG_MULTIVIEW_BUILD=true ;;
E )
USE_WERROR=true ;;
@ -221,16 +205,8 @@ if [ -z $VERBOSE ]; then
STRIP="> /dev/null 2>&1"
fi
if [ -z $APPVEYOR ]; then
echo "Running prebuild outside of Appveyor."
DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}")
cd $(dirname "$DIR")/..
else
echo "Running prebuild in Appveyor."
cd "$APPVEYOR_BUILD_FOLDER"
fi
DIR=$(windowsPathAsUnix "${BASH_SOURCE[0]}")
cd $(dirname "$DIR")/..
run_cmd() {
CMD="$1"
@ -241,13 +217,7 @@ run_cmd() {
eval $CMD $@ > output.log 2>&1 || RET=$?
if [ $RET -ne 0 ]; then
if [ -z $APPVEYOR ]; then
echo "Command $CMD failed, output can be found in $(real_pwd)/output.log"
else
echo
echo "Command $CMD failed;"
cat output.log
fi
else
rm output.log
fi
@ -304,6 +274,20 @@ download() {
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"
@ -357,6 +341,26 @@ add_qt_style_dlls() {
QT_STYLES[$CONFIG]="${QT_STYLES[$CONFIG]} $@"
}
declare -A QT_IMAGEFORMATS
QT_IMAGEFORMATS["Release"]=""
QT_IMAGEFORMATS["Debug"]=""
QT_IMAGEFORMATS["RelWithDebInfo"]=""
add_qt_image_dlls() {
local CONFIG=$1
shift
QT_IMAGEFORMATS[$CONFIG]="${QT_IMAGEFORMATS[$CONFIG]} $@"
}
declare -A QT_ICONENGINES
QT_ICONENGINES["Release"]=""
QT_ICONENGINES["Debug"]=""
QT_ICONENGINES["RelWithDebInfo"]=""
add_qt_icon_dlls() {
local CONFIG=$1
shift
QT_ICONENGINES[$CONFIG]="${QT_ICONENGINES[$CONFIG]} $@"
}
if [ -z $PLATFORM ]; then
PLATFORM="$(uname -m)"
fi
@ -368,38 +372,20 @@ fi
case $VS_VERSION in
17|17.0|2022 )
GENERATOR="Visual Studio 17 2022"
TOOLSET="vc143"
MSVC_TOOLSET="vc143"
MSVC_REAL_VER="17"
MSVC_VER="14.3"
MSVC_DISPLAY_YEAR="2022"
OSG_MSVC_YEAR="2019"
MYGUI_MSVC_YEAR="2019"
LUA_MSVC_YEAR="2019"
QT_MSVC_YEAR="2019"
BULLET_MSVC_YEAR="2019"
BOOST_VER="1.80.0"
BOOST_VER_URL="1_80_0"
BOOST_VER_SDK="108000"
;;
16|16.0|2019 )
GENERATOR="Visual Studio 16 2019"
TOOLSET="vc142"
MSVC_TOOLSET="vc142"
MSVC_REAL_VER="16"
MSVC_VER="14.2"
MSVC_DISPLAY_YEAR="2019"
OSG_MSVC_YEAR="2019"
MYGUI_MSVC_YEAR="2019"
LUA_MSVC_YEAR="2019"
QT_MSVC_YEAR="2019"
BULLET_MSVC_YEAR="2019"
BOOST_VER="1.80.0"
BOOST_VER_URL="1_80_0"
BOOST_VER_SDK="108000"
;;
15|15.0|2017 )
@ -534,42 +520,37 @@ if [ -n "$SINGLE_CONFIG" ]; then
add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}"
fi
if ! [ -z $UNITY_BUILD ]; then
if [[ -n "$UNITY_BUILD" ]]; then
add_cmake_opts "-DOPENMW_UNITY_BUILD=True"
fi
if ! [ -z $USE_CCACHE ]; then
add_cmake_opts "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
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 ! [ -z "$USE_WERROR" ]; then
if [[ -n "$USE_WERROR" ]]; then
add_cmake_opts "-DOPENMW_MSVC_WERROR=ON"
fi
if ! [ -z "$USE_CLANG_TIDY" ]; then
if [[ -n "$USE_CLANG_TIDY" ]]; then
add_cmake_opts "-DCMAKE_CXX_CLANG_TIDY=\"clang-tidy --warnings-as-errors=*\""
fi
BULLET_VER="2.89"
FFMPEG_VER="4.2.2"
ICU_VER="70_1"
LUAJIT_VER="v2.1.0-beta3-452-g7a0cf5fd"
LZ4_VER="1.9.2"
OPENAL_VER="1.23.0"
QT_VER="5.15.2"
OSG_ARCHIVE_NAME="OSGoS 3.6.5"
OSG_ARCHIVE="OSGoS-3.6.5-123-g68c5c573d-msvc${OSG_MSVC_YEAR}-win${BITS}"
OSG_ARCHIVE_REPO_URL="https://gitlab.com/OpenMW/openmw-deps/-/raw/main"
if ! [ -z $OSG_MULTIVIEW_BUILD ]; then
OSG_ARCHIVE_NAME="OSG-3.6-multiview"
OSG_ARCHIVE="OSG-3.6-multiview-d2ee5aa8-msvc${OSG_MSVC_YEAR}-win${BITS}"
OSG_ARCHIVE_REPO_URL="https://gitlab.com/madsbuvi/openmw-deps/-/raw/openmw-vr-ovr_multiview"
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 "==================================="
@ -577,7 +558,6 @@ echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}"
echo "==================================="
echo
# cd OpenMW/AppVeyor-test
mkdir -p deps
cd deps
@ -587,75 +567,17 @@ if [ -z $SKIP_DOWNLOAD ]; then
echo "Downloading dependency packages."
echo
# Boost
if [ -z $APPVEYOR ]; then
download "Boost ${BOOST_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \
"boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe"
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
# Bullet
download "Bullet ${BULLET_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z" \
"Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z"
# FFmpeg
download "FFmpeg ${FFMPEG_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-${FFMPEG_VER}-win${BITS}.zip" \
"ffmpeg-${FFMPEG_VER}-win${BITS}.zip" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip" \
"ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip"
# MyGUI
download "MyGUI 3.4.2" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z" \
"MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z"
if [ -n "$PDBS" ]; then
download "MyGUI symbols" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z" \
"MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z"
fi
# OpenAL
download "OpenAL-Soft ${OPENAL_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OpenAL-Soft-${OPENAL_VER}.zip" \
"OpenAL-Soft-${OPENAL_VER}.zip"
# OSGoS
download "${OSG_ARCHIVE_NAME}" \
"${OSG_ARCHIVE_REPO_URL}/windows/${OSG_ARCHIVE}.7z" \
"${OSG_ARCHIVE}.7z"
if [ -n "$PDBS" ]; then
download "${OSG_ARCHIVE_NAME} symbols" \
"${OSG_ARCHIVE_REPO_URL}/windows/${OSG_ARCHIVE}-sym.7z" \
"${OSG_ARCHIVE}-sym.7z"
fi
# SDL2
download "SDL 2.24.0" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-devel-2.24.0-VC.zip" \
"SDL2-devel-2.24.0-VC.zip"
# LZ4
download "LZ4 ${LZ4_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/lz4_win${BITS}_v${LZ4_VER//./_}.7z" \
"lz4_win${BITS}_v${LZ4_VER//./_}.7z"
# LuaJIT
download "LuaJIT ${LUAJIT_VER}" \
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z" \
"LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z"
# ICU
download "ICU ${ICU_VER/_/.}"\
"https://github.com/unicode-org/icu/releases/download/release-${ICU_VER/_/-}/icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip" \
"icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip"
download "zlib 1.2.11"\
"https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/zlib-1.2.11-msvc2017-win64.7z" \
"zlib-1.2.11-msvc2017-win64.7z"
fi
cd .. #/..
@ -691,186 +613,47 @@ echo "Extracting dependencies, this might take a while..."
echo "---------------------------------------------------"
echo
if [ -z $APPVEYOR ]; then
printf "Boost ${BOOST_VER}... "
else
printf "Boost ${BOOST_VER} AppVeyor... "
fi
cd $DEPS
echo
printf "vcpkg packages ${VCPKG_TAG:?}... "
{
if [ -z $APPVEYOR ]; then
cd $DEPS_INSTALL
BOOST_SDK="$(real_pwd)/Boost"
# Boost's installer is still based on ms-dos API that doesn't support larger than 260 char path names
# We work around this by installing to root of the current working drive and then move it to our deps
# get the current working drive's root, we'll install to that temporarily
CWD_DRIVE_ROOT="$(powershell -command '(get-location).Drive.Root')Boost_temp"
CWD_DRIVE_ROOT_BASH=$(windowsPathAsUnix "$CWD_DRIVE_ROOT")
if [ -d CWD_DRIVE_ROOT_BASH ]; then
printf "Cannot continue, ${CWD_DRIVE_ROOT_BASH} aka ${CWD_DRIVE_ROOT} already exists. Please remove before re-running. ";
wrappedExit 1;
fi
if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION ${BOOST_VER_SDK}" Boost/boost/version.hpp > /dev/null; then
if [[ -d "${VCPKG_PATH:?}" ]]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf Boost
CI_EXTRA_INNO_OPTIONS=""
[ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'"
"${DEPS}/boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS}
mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}"
fi
add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \
-DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}"
add_cmake_opts -DBoost_COMPILER="-${TOOLSET}"
echo Done.
else
# Appveyor has all the boost we need already
BOOST_SDK="c:/Libraries/boost_${BOOST_VER_URL}"
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 -DBOOST_ROOT="$BOOST_SDK" \
-DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.1"
add_cmake_opts -DBoost_COMPILER="-${TOOLSET}"
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"
echo Done.
fi
}
cd $DEPS
echo
printf "Bullet ${BULLET_VER}... "
{
cd $DEPS_INSTALL
if [ -d Bullet ]; then
printf -- "Exists. (No version checking) "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf Bullet
eval 7z x -y "${DEPS}/Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt.7z" $STRIP
mv "Bullet-${BULLET_VER}-msvc${BULLET_MSVC_YEAR}-win${BITS}-double-mt" Bullet
fi
add_cmake_opts -DBULLET_ROOT="$(real_pwd)/Bullet"
echo Done.
}
cd $DEPS
echo
printf "FFmpeg ${FFMPEG_VER}... "
{
cd $DEPS_INSTALL
if [ -d FFmpeg ] && grep "${FFMPEG_VER}" FFmpeg/README.txt > /dev/null; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf FFmpeg
eval 7z x -y "${DEPS}/ffmpeg-${FFMPEG_VER}-win${BITS}.zip" $STRIP
eval 7z x -y "${DEPS}/ffmpeg-${FFMPEG_VER}-dev-win${BITS}.zip" $STRIP
mv "ffmpeg-${FFMPEG_VER}-win${BITS}-shared" FFmpeg
cp -r "ffmpeg-${FFMPEG_VER}-win${BITS}-dev/"* FFmpeg/
rm -rf "ffmpeg-${FFMPEG_VER}-win${BITS}-dev"
fi
export FFMPEG_HOME="$(real_pwd)/FFmpeg"
for config in ${CONFIGURATIONS[@]}; do
add_runtime_dlls $config "$(pwd)/FFmpeg/bin/"{avcodec-58,avformat-58,avutil-56,swresample-3,swscale-5}.dll
done
if [ $BITS -eq 32 ]; then
add_cmake_opts "-DCMAKE_EXE_LINKER_FLAGS=\"/machine:X86 /safeseh:no\""
fi
echo Done.
}
cd $DEPS
echo
printf "MyGUI 3.4.2... "
{
cd $DEPS_INSTALL
if [ -d MyGUI ] && \
grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \
grep "MYGUI_VERSION_MINOR 4" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \
grep "MYGUI_VERSION_PATCH 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null
then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf MyGUI
eval 7z x -y "${DEPS}/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}.7z" $STRIP
[ -n "$PDBS" ] && eval 7z x -y "${DEPS}/MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}-sym.7z" $STRIP
mv "MyGUI-3.4.2-msvc${MYGUI_MSVC_YEAR}-win${BITS}" MyGUI
fi
export MYGUI_HOME="$(real_pwd)/MyGUI"
for CONFIGURATION in ${CONFIGURATIONS[@]}; do
if [ $CONFIGURATION == "Debug" ]; then
SUFFIX="_d"
MYGUI_CONFIGURATION="Debug"
if [[ ${CONFIGURATION:?} == "Debug" ]]; then
VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/debug/bin"
add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Debug/MyGUIEngine_d.dll"
else
SUFFIX=""
MYGUI_CONFIGURATION="RelWithDebInfo"
fi
add_runtime_dlls $CONFIGURATION "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll"
done
echo Done.
}
cd $DEPS
echo
printf "OpenAL-Soft ${OPENAL_VER}... "
{
if [ -d openal-soft-${OPENAL_VER}-bin ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf openal-soft-${OPENAL_VER}-bin
eval 7z x -y OpenAL-Soft-${OPENAL_VER}.zip $STRIP
fi
OPENAL_SDK="$(real_pwd)/openal-soft-${OPENAL_VER}-bin"
add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \
-DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib"
for config in ${CONFIGURATIONS[@]}; do
add_runtime_dlls $config "$(pwd)/openal-soft-${OPENAL_VER}-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll"
done
echo Done.
}
cd $DEPS
echo
printf "${OSG_ARCHIVE_NAME}... "
{
cd $DEPS_INSTALL
if [ -d OSG ] && \
grep "OPENSCENEGRAPH_MAJOR_VERSION 3" OSG/include/osg/Version > /dev/null && \
grep "OPENSCENEGRAPH_MINOR_VERSION 6" OSG/include/osg/Version > /dev/null && \
grep "OPENSCENEGRAPH_PATCH_VERSION 5" OSG/include/osg/Version > /dev/null
then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf OSG
eval 7z x -y "${DEPS}/${OSG_ARCHIVE}.7z" $STRIP
[ -n "$PDBS" ] && eval 7z x -y "${DEPS}/${OSG_ARCHIVE}-sym.7z" $STRIP
mv "${OSG_ARCHIVE}" OSG
fi
OSG_SDK="$(real_pwd)/OSG"
add_cmake_opts -DOSG_DIR="$OSG_SDK"
for CONFIGURATION in ${CONFIGURATIONS[@]}; do
if [ $CONFIGURATION == "Debug" ]; then
SUFFIX="d"
SUFFIX_UPCASE="D"
else
SUFFIX=""
SUFFIX_UPCASE=""
VCPKG_DLL_BIN="$(pwd)/${VCPKG_PATH:?}/installed/x64-windows/bin"
add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/Release/MyGUIEngine.dll"
fi
if ! [ -z $OSG_MULTIVIEW_BUILD ]; then
add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{ot21-OpenThreads,libpng16}${SUFFIX}.dll \
"$(pwd)/OSG/bin/osg162-osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow,Sim}${SUFFIX}.dll
else
add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,icuuc58,libpng16}${SUFFIX}.dll \
"$(pwd)/OSG/bin/libxml2"${SUFFIX_UPCASE}.dll \
"$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow,Sim}${SUFFIX}.dll
add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/icudt58.dll"
if [ $CONFIGURATION == "Debug" ]; then
add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{boost_system-vc141-mt-gd-1_63,collada-dom2.4-dp-vc141-mt-d}.dll
else
add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{boost_system-vc141-mt-1_63,collada-dom2.4-dp-vc141-mt}.dll
fi
fi
add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dae,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll
add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll
add_osg_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/osgPlugins-3.6.5/*.dll"
add_runtime_dlls ${CONFIGURATION:?} "${VCPKG_DLL_BIN:?}/*.dll"
done
echo Done.
}
cd $DEPS
echo
printf "Qt ${QT_VER}... "
@ -889,30 +672,11 @@ printf "Qt ${QT_VER}... "
if [ -d "Qt/${QT_VER}" ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
if [ $MISSINGPYTHON -ne 0 ]; then
echo "Can't be automatically installed without Python."
wrappedExit 1
fi
pushd "$DEPS" > /dev/null
if ! [ -d 'aqt-venv' ]; then
echo " Creating Virtualenv for aqt..."
run_cmd python -m venv aqt-venv
fi
if [ -d 'aqt-venv/bin' ]; then
VENV_BIN_DIR='bin'
elif [ -d 'aqt-venv/Scripts' ]; then
VENV_BIN_DIR='Scripts'
else
echo "Error: Failed to create virtualenv in expected location."
wrappedExit 1
fi
# check version
aqt-venv/${VENV_BIN_DIR}/pip list | grep 'aqtinstall\s*1.1.3' || [ $? -ne 0 ]
if [ $? -eq 0 ]; then
echo " Installing aqt wheel into virtualenv..."
run_cmd "aqt-venv/${VENV_BIN_DIR}/pip" install aqtinstall==1.1.3
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
@ -921,7 +685,7 @@ printf "Qt ${QT_VER}... "
mkdir Qt
cd Qt
run_cmd "${DEPS}/aqt-venv/${VENV_BIN_DIR}/aqt" install ${QT_VER} windows desktop "win${BITS}_msvc${QT_MSVC_YEAR}${SUFFIX}"
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}
@ -929,129 +693,39 @@ printf "Qt ${QT_VER}... "
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
add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \
-DCMAKE_PREFIX_PATH="$QT_SDK"
for CONFIGURATION in ${CONFIGURATIONS[@]}; do
if [ $CONFIGURATION == "Debug" ]; then
DLLSUFFIX="d"
else
DLLSUFFIX=""
fi
if [ "${QT_VER:0:1}" -eq "6" ]; then
add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,OpenGLWidgets,Widgets}${DLLSUFFIX}.dll
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_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_VER:0:1}"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll
fi
add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll"
add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll"
done
echo Done.
}
cd $DEPS
echo
printf "SDL 2.24.0... "
{
if [ -d SDL2-2.24.0 ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf SDL2-2.24.0
eval 7z x -y SDL2-devel-2.24.0-VC.zip $STRIP
fi
export SDL2DIR="$(real_pwd)/SDL2-2.24.0"
for config in ${CONFIGURATIONS[@]}; do
add_runtime_dlls $config "$(pwd)/SDL2-2.24.0/lib/x${ARCHSUFFIX}/SDL2.dll"
done
echo Done.
}
cd $DEPS
echo
printf "LZ4 ${LZ4_VER}... "
{
if [ -d LZ4_${LZ4_VER} ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf LZ4_${LZ4_VER}
eval 7z x -y lz4_win${BITS}_v${LZ4_VER//./_}.7z -o$(real_pwd)/LZ4_${LZ4_VER} $STRIP
fi
export LZ4DIR="$(real_pwd)/LZ4_${LZ4_VER}"
add_cmake_opts -DLZ4_INCLUDE_DIR="${LZ4DIR}/include" \
-DLZ4_LIBRARY="${LZ4DIR}/lib/liblz4.lib"
for CONFIGURATION in ${CONFIGURATIONS[@]}; do
if [ $CONFIGURATION == "Debug" ]; then
LZ4_CONFIGURATION="Debug"
else
SUFFIX=""
LZ4_CONFIGURATION="Release"
add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt${QT_MAJOR_VER}"{Core,Gui,Network,OpenGL,Widgets,Svg}${DLLSUFFIX}.dll
add_qt_style_dlls $CONFIGURATION "$(pwd)/plugins/styles/qwindowsvistastyle${DLLSUFFIX}.dll"
fi
add_runtime_dlls $CONFIGURATION "$(pwd)/LZ4_${LZ4_VER}/bin/${LZ4_CONFIGURATION}/liblz4.dll"
done
echo Done.
}
cd $DEPS
echo
printf "LuaJIT ${LUAJIT_VER}... "
{
if [ -d LuaJIT ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf LuaJIT
eval 7z x -y LuaJIT-${LUAJIT_VER}-msvc${LUA_MSVC_YEAR}-win${BITS}.7z -o$(real_pwd)/LuaJIT $STRIP
fi
export LUAJIT_DIR="$(real_pwd)/LuaJIT"
add_cmake_opts -DLuaJit_INCLUDE_DIR="${LUAJIT_DIR}/include" \
-DLuaJit_LIBRARY="${LUAJIT_DIR}/lib/lua51.lib"
for CONFIGURATION in ${CONFIGURATIONS[@]}; do
add_runtime_dlls $CONFIGURATION "$(pwd)/LuaJIT/bin/lua51.dll"
add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll"
add_qt_image_dlls $CONFIGURATION "$(pwd)/plugins/imageformats/qsvg${DLLSUFFIX}.dll"
add_qt_icon_dlls $CONFIGURATION "$(pwd)/plugins/iconengines/qsvgicon${DLLSUFFIX}.dll"
done
echo Done.
}
cd $DEPS
echo
printf "ICU ${ICU_VER/_/.}... "
{
if [ -d ICU-${ICU_VER} ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf ICU-${ICU_VER}
eval 7z x -y icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip -o$(real_pwd)/ICU-${ICU_VER} $STRIP
fi
ICU_ROOT="$(real_pwd)/ICU-${ICU_VER}"
add_cmake_opts -DICU_ROOT="${ICU_ROOT}" \
-DICU_INCLUDE_DIR="${ICU_ROOT}/include" \
-DICU_I18N_LIBRARY="${ICU_ROOT}/lib${BITS}/icuin.lib " \
-DICU_UC_LIBRARY="${ICU_ROOT}/lib${BITS}/icuuc.lib " \
-DICU_DEBUG=ON
for config in ${CONFIGURATIONS[@]}; do
add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icudt${ICU_VER/_*/}.dll"
add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icuin${ICU_VER/_*/}.dll"
add_runtime_dlls $config "$(pwd)/ICU-${ICU_VER}/bin${BITS}/icuuc${ICU_VER/_*/}.dll"
done
echo Done.
}
cd $DEPS
echo
printf "zlib 1.2.11... "
{
if [ -d zlib-1.2.11-msvc2017-win64 ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf zlib-1.2.11-msvc2017-win64
eval 7z x -y zlib-1.2.11-msvc2017-win64.7z $STRIP
fi
add_cmake_opts -DZLIB_ROOT="$(real_pwd)/zlib-1.2.11-msvc2017-win64"
for config in ${CONFIGURATIONS[@]}; do
if [ $config == "Debug" ]; then
add_runtime_dlls $config "$(pwd)/zlib-1.2.11-msvc2017-win64/bin/zlibd.dll"
else
add_runtime_dlls $config "$(pwd)/zlib-1.2.11-msvc2017-win64/bin/zlib.dll"
fi
done
echo Done.
}
add_cmake_opts -DCMAKE_PREFIX_PATH="\"${QT_SDK}\""
echo
cd $DEPS_INSTALL/..
@ -1137,6 +811,20 @@ fi
cp "$DLL" "${DLL_PREFIX}styles"
done
echo
echo "- Qt Image Format DLLs..."
mkdir -p ${DLL_PREFIX}imageformats
for DLL in ${QT_IMAGEFORMATS[$CONFIGURATION]}; do
echo " $(basename $DLL)"
cp "$DLL" "${DLL_PREFIX}imageformats"
done
echo
echo "- Qt Icon Engine DLLs..."
mkdir -p ${DLL_PREFIX}iconengines
for DLL in ${QT_ICONENGINES[$CONFIGURATION]}; do
echo " $(basename $DLL)"
cp "$DLL" "${DLL_PREFIX}iconengines"
done
echo
done
#fi
@ -1145,8 +833,9 @@ if [ "${BUILD_BENCHMARKS}" ]; then
fi
if [ -n "${TEST_FRAMEWORK}" ]; then
add_cmake_opts -DBUILD_UNITTESTS=ON
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

View file

@ -1,30 +1,46 @@
#!/bin/sh -e
export CXX=clang++
export CC=clang
# Silence a git warning
git config --global advice.detachedHead false
rm -fr build
mkdir build
cd build
DEPENDENCIES_ROOT="/tmp/openmw-deps"
QT_PATH=$(brew --prefix qt@5)
ICU_PATH=$(brew --prefix icu4c)
OPENAL_PATH=$(brew --prefix openal-soft)
CCACHE_EXECUTABLE=$(brew --prefix ccache)/bin/ccache
mkdir build
cd build
declare -a CMAKE_CONF_OPTS=(
-D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH;$OPENAL_PATH"
-D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE"
-D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE"
-D CMAKE_CXX_FLAGS="-stdlib=libc++"
-D CMAKE_C_COMPILER="clang"
-D CMAKE_CXX_COMPILER="clang++"
-D CMAKE_OSX_DEPLOYMENT_TARGET="13.6"
-D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE
-D Boost_INCLUDE_DIR="$DEPENDENCIES_ROOT/include"
-D OSGPlugins_LIB_DIR="$DEPENDENCIES_ROOT/lib/osgPlugins-3.6.5"
-D ICU_ROOT="$ICU_PATH"
-D OPENMW_OSX_DEPLOYMENT=TRUE
)
if [[ "${CMAKE_BUILD_TYPE}" ]]; then
CMAKE_CONF_OPTS+=(
-D CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
)
else
CMAKE_CONF_OPTS+=(
-D CMAKE_BUILD_TYPE=RelWithDebInfo
)
fi
cmake \
-D CMAKE_PREFIX_PATH="$DEPENDENCIES_ROOT;$QT_PATH" \
-D CMAKE_C_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \
-D CMAKE_CXX_COMPILER_LAUNCHER="$CCACHE_EXECUTABLE" \
-D CMAKE_CXX_FLAGS="-stdlib=libc++" \
-D CMAKE_C_FLAGS_RELEASE="-g -O0" \
-D CMAKE_CXX_FLAGS_RELEASE="-g -O0" \
-D CMAKE_OSX_DEPLOYMENT_TARGET="11" \
-D CMAKE_BUILD_TYPE=RELEASE \
-D OPENMW_OSX_DEPLOYMENT=TRUE \
-D OPENMW_USE_SYSTEM_RECASTNAVIGATION=TRUE \
"${CMAKE_CONF_OPTS[@]}" \
-D BUILD_OPENMW=TRUE \
-D BUILD_OPENCS=TRUE \
-D BUILD_ESMTOOL=TRUE \
@ -33,6 +49,5 @@ cmake \
-D BUILD_NIFTEST=TRUE \
-D BUILD_NAVMESHTOOL=TRUE \
-D BUILD_BULLETOBJECTTOOL=TRUE \
-D ICU_ROOT="$ICU_PATH" \
-G"Unix Makefiles" \
..

View file

@ -1,91 +0,0 @@
#!/bin/bash
APPVEYOR=""
CI=""
PACKAGE=""
PLATFORM=""
CONFIGURATION=""
VS_VERSION=""
if [ -z $PLATFORM ]; then
PLATFORM=`uname -m`
fi
if [ -z $CONFIGURATION ]; then
CONFIGURATION="Debug"
fi
case $VS_VERSION in
14|14.0|2015 )
GENERATOR="Visual Studio 14 2015"
MSVC_YEAR="2015"
MSVC_VER="14.0"
;;
# 12|2013|
* )
GENERATOR="Visual Studio 12 2013"
MSVC_YEAR="2013"
MVSC_VER="12.0"
;;
esac
case $PLATFORM in
x64|x86_64|x86-64|win64|Win64 )
BITS=64
;;
x32|x86|i686|i386|win32|Win32 )
BITS=32
;;
esac
case $CONFIGURATION in
debug|Debug|DEBUG )
CONFIGURATION=Debug
;;
release|Release|RELEASE )
CONFIGURATION=Release
;;
relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO )
CONFIGURATION=RelWithDebInfo
;;
esac
if [ -z $APPVEYOR ]; then
echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build outside of Appveyor."
DIR=$(echo "$0" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,")
cd $(dirname "$DIR")/..
else
echo "Running ${BITS}-bit MSVC${MSVC_YEAR} ${CONFIGURATION} build in Appveyor."
cd $APPVEYOR_BUILD_FOLDER
fi
BUILD_DIR="MSVC${MSVC_YEAR}_${BITS}"
cd ${BUILD_DIR}
which msbuild > /dev/null
if [ $? -ne 0 ]; then
msbuild() {
/c/Program\ Files\ \(x86\)/MSBuild/${MSVC_VER}/Bin/MSBuild.exe "$@"
}
fi
if [ -z $APPVEYOR ]; then
msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8
else
msbuild OpenMW.sln //t:Build //p:Configuration=${CONFIGURATION} //m:8 //logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll"
fi
RET=$?
if [ $RET -eq 0 ] && [ ! -z $PACKAGE ]; then
msbuild PACKAGE.vcxproj //t:Build //m:8
RET=$?
fi
exit $RET

11
CI/check_qt_translations.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash -ex
set -o pipefail
LUPDATE="${LUPDATE:-lupdate}"
${LUPDATE:?} -locations none apps/wizard -ts files/lang/wizard_*.ts
${LUPDATE:?} -locations none apps/launcher -ts files/lang/launcher_*.ts
${LUPDATE:?} -locations none components/contentselector components/process -ts files/lang/components_*.ts
! (git diff --name-only | grep -q "^") || (echo -e "\033[0;31mBuild a 'translations' CMake target to update Qt localization for these files:\033[0;0m"; git diff --name-only | xargs -i echo -e "\033[0;31m{}\033[0;0m"; exit -1)

View file

@ -1,9 +0,0 @@
#!/bin/bash
OUTPUT=$(grep -nRP '\t' --include=\*.{cpp,hpp,c,h} --exclude=ui_\* apps components)
if [[ $OUTPUT ]] ; then
echo "Error: Tab characters found!"
echo $OUTPUT
exit 1
fi

View file

@ -1,26 +0,0 @@
#!/bin/sh
# This script expect the following environment variables to be set:
# - OSX_DEPLOY_KEY: private SSH key, must be encoded like this before adding it to Travis secrets: https://github.com/travis-ci/travis-ci/issues/7715#issuecomment-433301692
# - OSX_DEPLOY_HOST: string specifying SSH of the following format: ssh-user@ssh-host
# - OSX_DEPLOY_PORT: SSH port, it can't be a part of the host string because scp doesn't accept hosts with ports
# - OSX_DEPLOY_HOST_FINGERPRINT: fingerprint of the host, can be obtained by using ssh-keygen -F [host]:port & putting it in double quotes when adding to Travis secrets
SSH_KEY_PATH="$HOME/.ssh/openmw_deploy"
REMOTE_PATH="\$HOME/nightly"
echo "$OSX_DEPLOY_KEY" > "$SSH_KEY_PATH"
chmod 600 "$SSH_KEY_PATH"
echo "$OSX_DEPLOY_HOST_FINGERPRINT" >> "$HOME/.ssh/known_hosts"
cd build || exit 1
DATE=$(date +'%d%m%Y')
SHORT_COMMIT=$(git rev-parse --short "${TRAVIS_COMMIT}")
TARGET_FILENAME="OpenMW-${DATE}-${SHORT_COMMIT}.dmg"
if ! ssh -p "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" "$OSX_DEPLOY_HOST" "ls \"$REMOTE_PATH\"" | grep "$SHORT_COMMIT" > /dev/null; then
scp -P "$OSX_DEPLOY_PORT" -i "$SSH_KEY_PATH" ./*.dmg "$OSX_DEPLOY_HOST:$REMOTE_PATH/$TARGET_FILENAME"
else
echo "An existing nightly build for commit ${SHORT_COMMIT} has been found, skipping upload."
fi

View file

@ -8,26 +8,26 @@ 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/openmw_test_suite/esm/test_fixed_string.cpp
apps/openmw_test_suite/files/conversion_tests.cpp
apps/openmw_test_suite/lua/test_async.cpp
apps/openmw_test_suite/lua/test_configuration.cpp
apps/openmw_test_suite/lua/test_l10n.cpp
apps/openmw_test_suite/lua/test_lua.cpp
apps/openmw_test_suite/lua/test_scriptscontainer.cpp
apps/openmw_test_suite/lua/test_serialization.cpp
apps/openmw_test_suite/lua/test_storage.cpp
apps/openmw_test_suite/lua/test_ui_content.cpp
apps/openmw_test_suite/lua/test_utilpackage.cpp
apps/openmw_test_suite/misc/test_endianness.cpp
apps/openmw_test_suite/misc/test_resourcehelpers.cpp
apps/openmw_test_suite/misc/test_stringops.cpp
apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp
apps/openmw_test_suite/mwscript/test_scripts.cpp
apps/openmw_test_suite/mwscript/test_utils.hpp
apps/openmw_test_suite/mwworld/test_store.cpp
apps/openmw_test_suite/openmw_test_suite.cpp
apps/openmw_test_suite/testing_util.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

View file

@ -33,9 +33,10 @@ declare -rA GROUPED_DEPS=(
libboost-system-dev libboost-iostreams-dev
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
libsdl2-dev libqt5opengl5-dev 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
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.
@ -86,7 +87,7 @@ declare -rA GROUPED_DEPS=(
libswresample3
libswscale5
libtinyxml2.6.2v5
libyaml-cpp0.7
libyaml-cpp0.8
python3-pip
xvfb
"
@ -99,6 +100,12 @@ declare -rA GROUPED_DEPS=(
clang-format-14
git-core
"
[openmw-qt-translations]="
qttools5-dev
qttools5-dev-tools
git-core
"
)
if [[ $# -eq 0 ]]; then
@ -122,4 +129,7 @@ mkdir -pv "$APT_CACHE_DIR"
apt-get update -yqq
apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends software-properties-common gnupg >/dev/null
add-apt-repository -y ppa:openmw/openmw
add-apt-repository -y ppa:openmw/openmw-daily
add-apt-repository -y ppa:openmw/staging
apt-get -qq -o dir::cache::archives="$APT_CACHE_DIR" install -y --no-install-recommends "${deps[@]}" >/dev/null
apt list --installed

View file

@ -125,8 +125,8 @@ modules:
- "-DMYGUI_BUILD_PLUGINS=0"
sources:
- type: archive
url: https://github.com/MyGUI/mygui/archive/refs/tags/MyGUI3.4.2.tar.gz
sha256: 1cc45fb96c9438e3476778449af0378443d84794a458978a29c75306e45dd45a
url: https://github.com/MyGUI/mygui/archive/refs/tags/MyGUI3.4.3.tar.gz
sha256: 33c91b531993047e77cace36d6fea73634b8c17bd0ed193d4cd12ac7c6328abd
- name: libunshield
buildsystem: cmake-ninja

View file

@ -1,6 +1,12 @@
#!/bin/bash -ex
git clone --depth=1 https://gitlab.com/OpenMW/example-suite.git
mkdir example-suite
cd example-suite
git init
git remote add origin https://gitlab.com/OpenMW/example-suite.git
git fetch --depth=1 origin ${EXAMPLE_SUITE_REVISION}
git checkout FETCH_HEAD
cd ..
xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \
scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/

View file

@ -1,14 +1,15 @@
set -e
#!/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 --depth 1
git clone https://github.com/teal-language/cyan.git
cd cyan
luarocks make cyan-dev-1.rockspec
git checkout v0.4.0
luarocks make cyan-0.4.0-1.rockspec
popd
cyan version
scripts/generate_teal_declarations.sh ./teal_declarations
zip teal_declarations.zip -r teal_declarations

View file

@ -31,7 +31,8 @@ cmake \
-D BUILD_OPENCS=ON \
-D BUILD_OPENCS_TESTS=ON \
-D BUILD_OPENMW=ON \
-D BUILD_UNITTESTS=ON \
-D BUILD_OPENMW_TESTS=ON \
-D BUILD_COMPONENTS_TESTS=ON \
-D BUILD_WIZARD=ON \
"${SRC}"
cmake --build . --parallel

View file

@ -19,6 +19,14 @@ 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)
@ -31,11 +39,13 @@ option(BUILD_ESMTOOL "Build ESM inspector" ON)
option(BUILD_NIFTEST "Build nif file tester" ON)
option(BUILD_DOCS "Build documentation." OFF )
option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF)
option(BUILD_UNITTESTS "Enable Unittests with Google C++ Unittest" OFF)
option(BUILD_COMPONENTS_TESTS "Build components library tests" OFF)
option(BUILD_BENCHMARKS "Build benchmarks with Google Benchmark" OFF)
option(BUILD_NAVMESHTOOL "Build navmesh tool" ON)
option(BUILD_BULLETOBJECTTOOL "Build Bullet object tool" ON)
option(BUILD_OPENCS_TESTS "Build OpenMW Construction Set tests" OFF)
option(BUILD_OPENMW_TESTS "Build OpenMW tests" OFF)
option(PRECOMPILE_HEADERS_WITH_MSVC "Precompile most common used headers with MSVC (alternative to ccache)" ON)
set(OpenGL_GL_PREFERENCE LEGACY) # Use LEGACY as we use GL2; GLNVD is for GL3 and up.
@ -54,6 +64,7 @@ IF(NOT CMAKE_BUILD_TYPE)
ENDIF()
if (APPLE)
set(CMAKE_FIND_FRAMEWORK LAST) # prefer dylibs over frameworks
set(APP_BUNDLE_NAME "${CMAKE_PROJECT_NAME}.app")
set(APP_BUNDLE_DIR "${OpenMW_BINARY_DIR}/${APP_BUNDLE_NAME}")
@ -71,6 +82,8 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 49)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 68)
set(OPENMW_POSTPROCESSING_API_REVISION 2)
set(OPENMW_VERSION_COMMITHASH "")
set(OPENMW_VERSION_TAGHASH "")
@ -78,7 +91,7 @@ set(OPENMW_VERSION_COMMITDATE "")
set(OPENMW_VERSION "${OPENMW_VERSION_MAJOR}.${OPENMW_VERSION_MINOR}.${OPENMW_VERSION_RELEASE}")
set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/stable/")
set(OPENMW_DOC_BASEURL "https://openmw.readthedocs.io/en/")
set(GIT_CHECKOUT FALSE)
if(EXISTS ${PROJECT_SOURCE_DIR}/.git)
@ -111,8 +124,6 @@ include(WholeArchive)
configure_file ("${OpenMW_SOURCE_DIR}/docs/mainpage.hpp.cmake" "${OpenMW_BINARY_DIR}/docs/mainpage.hpp")
option(BOOST_STATIC "Link static build of Boost into the binaries" FALSE)
option(SDL2_STATIC "Link static build of SDL into the binaries" FALSE)
option(QT_STATIC "Link static build of Qt into the binaries" FALSE)
option(OPENMW_USE_SYSTEM_BULLET "Use system provided bullet physics library" ON)
@ -142,7 +153,7 @@ option(MYGUI_STATIC "Link static build of Mygui into the binaries" ${_mygui_stat
option(OPENMW_USE_SYSTEM_RECASTNAVIGATION "Use system provided recastnavigation library" OFF)
if(OPENMW_USE_SYSTEM_RECASTNAVIGATION)
set(_recastnavigation_static_default OFF)
find_package(RecastNavigation REQUIRED)
find_package(RecastNavigation REQUIRED CONFIG)
else()
set(_recastnavigation_static_default ON)
endif()
@ -156,13 +167,6 @@ option(OPENMW_USE_SYSTEM_GOOGLETEST "Use system Google Test library." OFF)
option(OPENMW_UNITY_BUILD "Use fewer compilation units to speed up compile time" FALSE)
option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF)
# what is necessary to build documentation
IF( BUILD_DOCS )
# Builds the documentation.
FIND_PACKAGE( Sphinx REQUIRED )
FIND_PACKAGE( Doxygen REQUIRED )
ENDIF()
# OS X deployment
option(OPENMW_OSX_DEPLOYMENT OFF)
@ -178,6 +182,24 @@ if (MSVC)
# 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
@ -205,7 +227,7 @@ else()
endif(APPLE)
if (WIN32)
option(USE_DEBUG_CONSOLE "whether a debug console should be enabled for debug builds, if false debug output is redirected to Visual Studio output" ON)
option(USE_DEBUG_CONSOLE "Whether a console should be displayed if OpenMW isn't launched from the command line. Does not affect the Release configuration." ON)
endif()
if(MSVC)
@ -213,16 +235,25 @@ if(MSVC)
endif()
# Dependencies
find_package(OpenGL REQUIRED)
if (APPLE)
# Force CMake to use the installed version of OpenGL on macOS
set(_SAVE_CMAKE_FIND_FRAMEWORK ${CMAKE_FIND_FRAMEWORK})
set(CMAKE_FIND_FRAMEWORK ONLY)
find_package(OpenGL REQUIRED)
set(CMAKE_FIND_FRAMEWORK ${_SAVE_CMAKE_FIND_FRAMEWORK})
unset(_SAVE_CMAKE_FIND_FRAMEWORK)
else()
find_package(OpenGL REQUIRED)
endif()
find_package(LZ4 REQUIRED)
if (USE_QT)
find_package(QT REQUIRED COMPONENTS Core NAMES Qt6 Qt5)
if (QT_VERSION_MAJOR VERSION_EQUAL 5)
find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL REQUIRED)
find_package(Qt5 5.15 COMPONENTS Core Widgets Network OpenGL LinguistTools Svg REQUIRED)
else()
find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets REQUIRED)
find_package(Qt6 COMPONENTS Core Widgets Network OpenGL OpenGLWidgets LinguistTools Svg REQUIRED)
endif()
message(STATUS "Using Qt${QT_VERSION}")
endif()
@ -276,7 +307,7 @@ if (OPENMW_USE_SYSTEM_YAML_CPP)
find_package(yaml-cpp REQUIRED)
endif()
if ((BUILD_UNITTESTS OR BUILD_OPENCS_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST)
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()
@ -293,8 +324,8 @@ find_package(FFmpeg REQUIRED COMPONENTS AVCODEC AVFORMAT AVUTIL SWSCALE SWRESAMP
if(FFmpeg_FOUND)
SET(FFVER_OK TRUE)
# Can not detect FFmpeg version on Windows for now
if (NOT WIN32)
# Can not detect FFmpeg version on Windows or Android for now
if (NOT WIN32 AND NOT ANDROID)
if(FFmpeg_AVFORMAT_VERSION VERSION_LESS "57.56.100")
message(STATUS "libavformat is too old! (${FFmpeg_AVFORMAT_VERSION}, wanted 57.56.100)")
set(FFVER_OK FALSE)
@ -346,11 +377,6 @@ endif()
# Platform specific
if (WIN32)
if(NOT MINGW)
set(Boost_USE_STATIC_LIBS ON)
add_definitions(-DBOOST_ALL_NO_LIB)
endif(NOT MINGW)
# Suppress WinMain(), provided by SDL
add_definitions(-DSDL_MAIN_HANDLED)
@ -433,32 +459,14 @@ if(HAVE_MULTIVIEW)
add_definitions(-DOSG_HAS_MULTIVIEW)
endif(HAVE_MULTIVIEW)
set(BOOST_COMPONENTS system program_options iostreams)
if(WIN32)
set(BOOST_COMPONENTS ${BOOST_COMPONENTS} locale)
if(MSVC)
# boost-zlib is not present (nor needed) in vcpkg version of boost.
# there, it is part of boost-iostreams instead.
set(BOOST_OPTIONAL_COMPONENTS zlib)
endif(MSVC)
endif(WIN32)
set(BOOST_COMPONENTS iostreams program_options system)
IF(BOOST_STATIC)
set(Boost_USE_STATIC_LIBS ON)
endif()
set(Boost_NO_BOOST_CMAKE ON)
find_package(Boost 1.6.2 REQUIRED COMPONENTS ${BOOST_COMPONENTS} OPTIONAL_COMPONENTS ${BOOST_OPTIONAL_COMPONENTS})
if(Boost_VERSION_STRING VERSION_GREATER_EQUAL 1.77.0)
find_package(Boost 1.77.0 REQUIRED COMPONENTS atomic)
endif()
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.2 REQUIRED)
find_package(MyGUI 3.4.3 REQUIRED)
endif()
find_package(SDL2 2.0.9 REQUIRED)
find_package(SDL2 2.0.10 REQUIRED)
find_package(OpenAL REQUIRED)
find_package(ZLIB REQUIRED)
@ -471,7 +479,7 @@ else(USE_LUAJIT)
find_package(Lua REQUIRED)
add_compile_definitions(NO_LUAJIT)
endif(USE_LUAJIT)
if (NOT WIN32)
if (NOT WIN32 AND NOT ANDROID)
include(cmake/CheckLuaCustomAllocator.cmake)
endif()
@ -482,8 +490,6 @@ set(SOL_CONFIG_DIR ${OpenMW_SOURCE_DIR}/extern/sol_config)
include_directories(
BEFORE SYSTEM
"."
${SDL2_INCLUDE_DIR}
${Boost_INCLUDE_DIR}
${MyGUI_INCLUDE_DIRS}
${OPENAL_INCLUDE_DIR}
${OPENGL_INCLUDE_DIR}
@ -494,7 +500,7 @@ include_directories(
${ICU_INCLUDE_DIRS}
)
link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS} ${COLLADA_DOM_LIBRARY_DIRS})
link_directories(${COLLADA_DOM_LIBRARY_DIRS})
if(MYGUI_STATIC)
add_definitions(-DMYGUI_STATIC)
@ -536,14 +542,19 @@ pack_resource_file(${OpenMW_SOURCE_DIR}/files/settings-default.cfg
configure_resource_file(${OpenMW_SOURCE_DIR}/files/openmw.appdata.xml
"${OpenMW_BINARY_DIR}" "openmw.appdata.xml")
if (NOT APPLE)
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")
else ()
configure_file(${OpenMW_SOURCE_DIR}/files/openmw.cfg
"${OpenMW_BINARY_DIR}/openmw.cfg")
endif ()
pack_resource_file(${OpenMW_SOURCE_DIR}/files/openmw-cs.cfg
@ -582,7 +593,6 @@ 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}")
add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON )
if (APPLE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++")
@ -617,7 +627,7 @@ endif()
add_subdirectory (components)
# Apps and tools
if (BUILD_OPENMW)
if (BUILD_OPENMW OR BUILD_OPENMW_TESTS)
add_subdirectory( apps/openmw )
endif()
@ -651,11 +661,10 @@ endif()
if (BUILD_NIFTEST)
add_subdirectory(apps/niftest)
endif(BUILD_NIFTEST)
endif()
# UnitTests
if (BUILD_UNITTESTS)
add_subdirectory( apps/openmw_test_suite )
if (BUILD_COMPONENTS_TESTS)
add_subdirectory(apps/components_tests)
endif()
if (BUILD_BENCHMARKS)
@ -667,13 +676,17 @@ if (BUILD_NAVMESHTOOL)
endif()
if (BUILD_BULLETOBJECTTOOL)
add_subdirectory( apps/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} )
@ -685,9 +698,8 @@ if (WIN32)
if (USE_DEBUG_CONSOLE AND BUILD_OPENMW)
set_target_properties(openmw PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
set_target_properties(openmw PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE")
set_target_properties(openmw PROPERTIES COMPILE_DEFINITIONS $<$<CONFIG:Debug>:_CONSOLE>)
elseif (BUILD_OPENMW)
# Turn off debug console, debug output will be written to visual studio output instead
# 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()
@ -710,67 +722,74 @@ if (WIN32)
)
foreach(d ${WARNINGS_DISABLE})
set(WARNINGS "${WARNINGS} /wd${d}")
list(APPEND WARNINGS "/wd${d}")
endforeach(d)
if(OPENMW_MSVC_WERROR)
set(WARNINGS "${WARNINGS} /WX")
list(APPEND WARNINGS "/WX")
endif()
set_target_properties(components PROPERTIES COMPILE_FLAGS "${WARNINGS}")
set_target_properties(osg-ffmpeg-videoplayer PROPERTIES COMPILE_FLAGS "${WARNINGS}")
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)
set_target_properties(bsatool PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(bsatool PRIVATE ${WARNINGS})
endif()
if (BUILD_ESMTOOL)
set_target_properties(esmtool PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(esmtool PRIVATE ${WARNINGS})
endif()
if (BUILD_ESSIMPORTER)
set_target_properties(openmw-essimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-essimporter PRIVATE ${WARNINGS})
endif()
if (BUILD_LAUNCHER)
set_target_properties(openmw-launcher PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-launcher PRIVATE ${WARNINGS})
endif()
if (BUILD_MWINIIMPORTER)
set_target_properties(openmw-iniimporter PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-iniimporter PRIVATE ${WARNINGS})
endif()
if (BUILD_OPENCS)
set_target_properties(openmw-cs PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-cs PRIVATE ${WARNINGS})
endif()
if (BUILD_OPENMW)
set_target_properties(openmw PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw PRIVATE ${WARNINGS})
endif()
if (BUILD_WIZARD)
set_target_properties(openmw-wizard PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-wizard PRIVATE ${WARNINGS})
endif()
if (BUILD_UNITTESTS)
set_target_properties(openmw_test_suite PROPERTIES COMPILE_FLAGS "${WARNINGS}")
if (BUILD_COMPONENTS_TESTS)
target_compile_options(components-tests PRIVATE ${WARNINGS})
endif()
if (BUILD_BENCHMARKS)
set_target_properties(openmw_detournavigator_navmeshtilescache_benchmark PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE ${WARNINGS})
endif()
if (BUILD_NAVMESHTOOL)
set_target_properties(openmw-navmeshtool PROPERTIES COMPILE_FLAGS "${WARNINGS}")
target_compile_options(openmw-navmeshtool PRIVATE ${WARNINGS})
endif()
if (BUILD_BULLETOBJECTTOOL)
set(WARNINGS "${WARNINGS} ${MT_BUILD}")
set_target_properties(openmw-bulletobjecttool PROPERTIES COMPILE_FLAGS "${WARNINGS}")
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)
@ -802,6 +821,18 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE)
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)
@ -809,6 +840,8 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE)
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 ()
@ -877,6 +910,11 @@ if (OPENMW_OSX_DEPLOYMENT AND APPLE)
install_plugins_for_bundle("${APP_BUNDLE_NAME}" PLUGINS)
install_plugins_for_bundle("${OPENCS_BUNDLE_NAME}" OPENCS_PLUGINS)
INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "/${INSTALLED_OPENMW_APP}/..")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "/${INSTALLED_OPENMW_APP}/..")
set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}")
set(PLUGINS ${PLUGINS} "${INSTALLED_OPENMW_APP}/Contents/PlugIns/${QT_QMACSTYLE_PLUGIN_GROUP}/${QT_QMACSTYLE_PLUGIN_NAME}")
set(OPENCS_PLUGINS ${OPENCS_PLUGINS} "${INSTALLED_OPENCS_APP}/Contents/PlugIns/${QT_COCOA_PLUGIN_GROUP}/${QT_COCOA_PLUGIN_NAME}")
@ -928,6 +966,7 @@ elseif(NOT APPLE)
INSTALL(FILES "${OpenMW_SOURCE_DIR}/CHANGELOG.md" DESTINATION "." RENAME "CHANGELOG.txt")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/README.md" DESTINATION "." RENAME "README.txt")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/LICENSE" DESTINATION "." RENAME "LICENSE.txt")
INSTALL(FILES "${OpenMW_SOURCE_DIR}/AUTHORS.md" DESTINATION "." RENAME "AUTHORS.txt")
INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION ".")
INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION ".")
@ -1041,7 +1080,6 @@ elseif(NOT APPLE)
# Install global configuration files
INSTALL(FILES "${INSTALL_SOURCE}/defaults.bin" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
INSTALL(FILES "${INSTALL_SOURCE}/openmw.cfg.install" DESTINATION "${SYSCONFDIR}" RENAME "openmw.cfg" COMPONENT "openmw")
INSTALL(FILES "${INSTALL_SOURCE}/resources/version" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
INSTALL(FILES "${INSTALL_SOURCE}/gamecontrollerdb.txt" DESTINATION "${SYSCONFDIR}" COMPONENT "openmw")
IF(BUILD_OPENCS)
@ -1053,12 +1091,17 @@ elseif(NOT APPLE)
endif(WIN32)
endif(OPENMW_OSX_DEPLOYMENT AND APPLE)
# Doxygen Target -- simply run 'make doc' or 'make doc_pages'
# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen"
# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined
# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise
find_package(Doxygen)
if (DOXYGEN_FOUND)
# what is necessary to build documentation
if ( BUILD_DOCS )
# Builds the documentation.
FIND_PACKAGE( Sphinx REQUIRED )
FIND_PACKAGE( Doxygen REQUIRED )
# Doxygen Target -- simply run 'make doc' or 'make doc_pages'
# output directory for 'make doc' is "${OpenMW_BINARY_DIR}/docs/Doxygen"
# output directory for 'make doc_pages' is "${DOXYGEN_PAGES_OUTPUT_DIR}" if defined
# or "${OpenMW_BINARY_DIR}/docs/Pages" otherwise
# determine output directory for doc_pages
if (NOT DEFINED DOXYGEN_PAGES_OUTPUT_DIR)
set(DOXYGEN_PAGES_OUTPUT_DIR "${OpenMW_BINARY_DIR}/docs/Pages")
@ -1075,3 +1118,78 @@ if (DOXYGEN_FOUND)
WORKING_DIRECTORY ${OpenMW_BINARY_DIR}
COMMENT "Generating documentation for the github-pages at ${DOXYGEN_PAGES_OUTPUT_DIR}" VERBATIM)
endif ()
# Qt localization
if (USE_QT)
file(GLOB LAUNCHER_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/launcher_*.ts)
file(GLOB WIZARD_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/wizard_*.ts)
file(GLOB COMPONENTS_TS_FILES ${CMAKE_SOURCE_DIR}/files/lang/components_*.ts)
get_target_property(QT_LUPDATE_EXECUTABLE Qt::lupdate IMPORTED_LOCATION)
add_custom_target(translations
COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/components/contentselector ${CMAKE_SOURCE_DIR}/components/process -ts ${COMPONENTS_TS_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/components
VERBATIM
COMMAND_EXPAND_LISTS
COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/wizard -ts ${WIZARD_TS_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/wizard
VERBATIM
COMMAND_EXPAND_LISTS
COMMAND ${QT_LUPDATE_EXECUTABLE} -locations none ${CMAKE_SOURCE_DIR}/apps/launcher -ts ${LAUNCHER_TS_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/apps/launcher
VERBATIM
COMMAND_EXPAND_LISTS)
if (BUILD_LAUNCHER OR BUILD_WIZARD)
if (APPLE)
set(QT_OPENMW_TRANSLATIONS_PATH "${APP_BUNDLE_DIR}/Contents/Resources/resources/translations")
else ()
get_generator_is_multi_config(multi_config)
if (multi_config)
set(QT_OPENMW_TRANSLATIONS_PATH "${OpenMW_BINARY_DIR}/$<CONFIG>/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()

View file

@ -8,37 +8,37 @@ https://gitlab.com/OpenMW/openmw/issues
Currently, we are focused on completing the MW game experience and general polishing. Features out of this scope may be approved in some cases, but you should probably start a discussion first.
Note:
* Tasks set to 'openmw-future' are usually out of the current scope of the project and can't be started yet.
* Bugs that are not 'Confirmed' should be confirmed first.
* Issues that have the 'Future' label are usually out of the current scope of the project. Corresponding submissions are unlikely to be merged or even properly reviewed.
* Newly reported bugs should be attempted to be reproduced on the latest code and on the latest available stable release. Both can be found [here](https://openmw.org/downloads/).
* Often, it's best to start a discussion about possible solutions before you jump into coding, especially for larger features.
Aside from coding, you can also help by triaging the issues list. Check for bugs that are 'Unconfirmed' and try to confirm them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug reporting guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so!
Aside from coding, you can also help by triaging the issues list. Check for unconfirmed bugs and try to reproduce them on your end, working out any details that may be necessary. Check for bugs that do not conform to [Bug Reporting Guidelines](https://wiki.openmw.org/index.php?title=Bug_Reporting_Guidelines) and improve them to do so!
There are various [Tools](https://wiki.openmw.org/index.php?title=Tools) to facilitate testing/development.
Pull Request Guidelines
Merge request guidelines
=======================
To facilitate the review process, your pull request description should include the following, if applicable:
To facilitate the review process, your merge request description should include the following, if applicable:
* A link back to the bug report or forum discussion that prompted the change. Note: when linking bugs, use the syntax ```[Bug #xyz](https://gitlab.com/OpenMW/openmw/issues/#xyz)``` to create a clickable link. Writing only 'Bug #xyz' will unfortunately create a link to the Github pull request with that number instead.
* A link back to the bug report or discussion that prompted the change.
* Summary of the changes made.
* Reasoning / motivation behind the change.
* What testing you have carried out to verify the change.
Furthermore, we advise to:
* Avoid stuffing unrelated commits into one pull request. As a rule of thumb, each feature and each bugfix should go into a separate PR, unless they are closely related or dependent upon each other. Small pull requests are easier to review, and are less likely to require further changes before we can merge them. A "mega" pull request with lots of unrelated commits in it is likely to get held up in review for a long time.
* Feel free to submit incomplete pull requests. Even if the work can not be merged yet, pull requests are a great place to collect early feedback. Just make sure to mark it as *[Incomplete]* or *[Do not merge yet]* in the title.
* Avoid stuffing unrelated commits into one merge request. As a rule of thumb, each feature and each bugfix should go into a separate MR, unless they are closely related or dependent upon each other. Small merge requests are easier to review and are less likely to require further changes before we can merge them. A "mega" merge request with lots of unrelated commits in it is likely to get held up in review for a long time.
* Feel free to submit incomplete merge requests. Even if the work cannot be merged yet, merge requests are a great place to collect early feedback. Just make sure to mark it as [draft](https://docs.gitlab.com/ee/user/project/merge_requests/drafts/).
* If you plan on contributing often, please read the [Developer Reference](https://wiki.openmw.org/index.php?title=Developer_Reference) on our wiki, especially the [Policies and Standards](https://wiki.openmw.org/index.php?title=Policies_and_Standards).
* Make sure each of your changes has a clear objective. Unnecessary changes may lead to merge conflicts, clutter the commit history and slow down review. Code formatting 'fixes' should be avoided, unless you were already changing that particular line anyway.
* Reference the bug / feature ticket(s) in your commit message (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your commit message includes 'Fixes #123', that bug/feature will automatically be set to 'Closed' when your commit is merged.
* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running PRs.
* Reference the bug / feature ticket(s) in your commit message or merge request description (e.g. 'Bug #123') to make it easier to keep track of what we changed for what reason. Our bugtracker will show those commits next to the ticket. If your merge request's description includes 'Fixes #123', that issue will automatically be closed when your commit is merged.
* When pulling changes from master, prefer rebase over merge. Consider using a merge if there are conflicts or for long-running MRs.
Guidelines for original engine "fixes"
=================================
From time to time you may be tempted to "fix" what you think was a "bug" in the original game engine.
From time to time, you may be tempted to "fix" what you think was a "bug" in the original game engine.
Unfortunately, the definition of what is a "bug" is not so clear. Consider that your "bug" is actually a feature unless proven otherwise:
@ -52,56 +52,62 @@ OpenMW, in its default configuration, is meant to be a faithful reimplementation
That said, we may sometimes evaluate such issues on an individual basis. Common exceptions to the above would be:
* Issues so glaring that they would severely limit the capabilities of the engine in the future (for example, the scripting engine not being allowed to access objects in remote cells).
* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug were being tired made it easier to repair items, instead of harder).
* Bugs where the intent is very obvious, and that have little to no balancing impact (e.g. the bug where being tired made it easier to repair items, instead of harder).
* Bugs that were fixed in an official patch for Morrowind.
Feature additions policy
=====================
We get it, you have waited so long for feature XYZ to be available in Morrowind and now that OpenMW is here you can not wait to implement your ingenious idea and share it with the world.
We get it: you have waited so long for feature XYZ to be available in Morrowind, and now that OpenMW is here, you cannot wait to implement your ingenious idea and share it with the world.
Unfortunately, since maintaining features comes at a cost and our resources are limited, we have to be a little selective in what features we allow into the main repository. Generally:
* Features should be as generic and non-redundant as possible.
* Any feature that is also possible with modding should be done as a mod instead.
* In the future, OpenMW plans to expand the scope of what is possible with modding, e.g. by moving certain game logic into editable scripts.
* Currently, modders can edit OpenMW's GUI skins and layout XML files, although there are still a few missing hooks (e.g. scripting support) in order to make this into a powerful way of modding.
* If a feature introduces new game UI strings, that reduces its chance of being accepted because we do not currently have any way of localizing these to the user's Morrowind installation language.
* Through moving certain game logic into built-in scripting, OpenMW will expand the scope of what is possible with modding.
* Modders can edit OpenMW's GUI skins and layout XML files as well as create new widgets through the Lua API, but it is expected that existing C++ widgets will also be recreated through built-in scripting.
* If a feature introduces new game UI strings, you will need to become acquainted with OpenMW's YAML localisation system and expose them. Read about it [here](https://openmw.readthedocs.io/en/latest/reference/modding/localisation.html).
If you are in doubt of your feature being within our scope, it is probably best to start a forum discussion first. See the [settings documentation](https://openmw.readthedocs.io/en/stable/reference/modding/settings/index.html) and [Features list](https://wiki.openmw.org/index.php?title=Features) for some examples of features that were deemed acceptable.
Reviewing pull requests
Reviewing merge requests
=======================
We welcome any help in reviewing open PRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/).
We welcome any help in reviewing open MRs. You don't need to be a developer to comment on new features. We also encourage ["junior" developers to review senior's work](https://pagefault.blog/2018/04/08/why-junior-devs-should-review-seniors-commits/).
This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the PR is deemed OK. Anyone can help with the Functionality Review while most parts of the Code Review require you to have programming experience.
This review process is divided into two sections because complaining about code or style issues hardly makes sense until the functionality of the MR is deemed OK. Anyone can help with the **functionality review** while most parts of the **code review** require you to have programming experience.
In addition to the checklist below, make sure to check that the Pull Request Guidelines (first half of this document) were followed.
In addition to the checklist below, make sure to check that the **merge request guidelines** (first half of this document) were followed.
First review
Functionality review
============
1. Ask for missing information or clarifications. Compare against the project's design goals and roadmap.
2. Check if the automated tests are passing. If they are not, make the PR author aware of the issue and potentially link to the error line on Travis CI or Appveyor. If the error appears unrelated to the PR and/or the master branch is failing with the same error, our CI has broken and needs to be fixed independently of any open PRs. Raise this issue on the forums, bug tracker or with the relevant maintainer. The PR can be merged in this case as long as you've built it yourself to make sure it does build.
3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a PR that no one has tested so far, post a comment letting us know.
4. On long running PRs, request the author to update its description with the current state or a checklist of things left to do.
2. Check if the automated tests are passing. If they are not, make the MR author aware of the issue and potentially quote the error line. If the error appears unrelated to the MR and/or the master branch is failing with the same error, our CI might be broken and needs to be fixed independently of any open MRs. Raise this issue on one of the following resources:
* Our [forums](https://forum.openmw.org/)
* [Discord](https://discord.com/servers/openmw-260439894298460160)
* [IRC](https://web.libera.chat/#openmw)
* [Issue tracker](https://gitlab.com/OpenMW/openmw/-/issues)
Code Review
3. Make sure that the new code has been tested thoroughly, either by asking the author or, preferably, testing yourself. In a complex project like OpenMW, it is easy to make mistakes, typos, etc. Therefore, prefer testing all code changes, no matter how trivial they look. When you have tested a MR that no one has tested so far, post a comment letting us know.
4. On long-running MRs, request the author to update its description with the current state or a checklist of things left to do.
Code review
===========
1. Carefully review each line for issues the author may not have thought of, paying special attention to 'special' cases. Often, people build their code with a particular mindset and forget about other configurations or unexpected interactions.
2. If any changes are workarounds for an issue in an upstream library, make sure the issue was reported upstream so we can eventually drop the workaround when the issue is fixed and the new version of that library is a build dependency.
3. Make sure PRs do not turn into arguments about hardly related issues. If the PR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the PR to adhere to the established way, rather than leaving the PR hanging on a dispute.
3. Make sure MRs do not turn into arguments about hardly related issues. If the MR author disagrees with an established part of the project (e.g. supported build environments), they should open a forum discussion or bug report and in the meantime adjust the MR to adhere to the established way, rather than leaving the MR hanging on a dispute.
4. Check if the code matches our style guidelines.
5. Check to make sure the commit history is clean. Squashing should be considered if the review process has made the commit history particularly long. Commits that don't build should be avoided because they are a nuisance for ```git bisect```.
Merging
=======
To be able to merge PRs, commit 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".
To be able to merge MRs, commit privileges are required. If you do not have the privileges, just ping someone that does have them with a short comment like "Looks good to me @user".
The person to merge the PR may either use github's Merge button or if using the command line, use the ```--no-ff``` flag (so a merge commit is created, just like with Github's merge button) and include the pull request number in the commit description.
In general case, you should not merge MRs prematurely even if you are sure they just work or if they receive a senior member's approval.
The rule of thumb is to give at least 24 hours to a couple days of a window for reviews to come through. For more technically involved MRs, 24 hours might not be enough.
Dealing with regressions
========================

View file

@ -20,7 +20,7 @@ Font Licenses:
Current Status
--------------
The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/issues?label_name%5B%5D=1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces.
The main quests in Morrowind, Tribunal and Bloodmoon are all completable. Some issues with side quests are to be expected (but rare). Check the [bug tracker](https://gitlab.com/OpenMW/openmw/-/issues/?milestone_title=openmw-1.0) for a list of issues we need to resolve before the "1.0" release. Even before the "1.0" release however, OpenMW boasts some new [features](https://wiki.openmw.org/index.php?title=Features), such as improved graphics and user interfaces.
Pre-existing modifications created for the original Morrowind engine can be hit-and-miss. The OpenMW script compiler performs more thorough error-checking than Morrowind does, meaning that a mod created for Morrowind may not necessarily run in OpenMW. Some mods also rely on quirky behaviour or engine bugs in order to work. We are considering such compatibility issues on a case-by-case basis - in some cases adding a workaround to OpenMW may be feasible, in other cases fixing the mod will be the only option. If you know of any mods that work or don't work, feel free to add them to the [Mod status](https://wiki.openmw.org/index.php?title=Mod_status) wiki page.
@ -74,10 +74,6 @@ Command line options
1 - show warning but consider script as
correctly compiled anyway
2 - treat warnings as errors
--script-blacklist arg ignore the specified script (if the use
of the blacklist is enabled)
--script-blacklist-use [=arg(=1)] (=1)
enable script blacklisting
--load-savegame arg load a save game file on game startup
(specify an absolute filename or a
filename relative to the current

View file

@ -5,7 +5,7 @@ if (UNIX AND NOT APPLE)
target_link_libraries(openmw_detournavigator_navmeshtilescache_benchmark ${CMAKE_THREAD_LIBS_INIT})
endif()
if (MSVC)
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(openmw_detournavigator_navmeshtilescache_benchmark PRIVATE <algorithm>)
endif()

View file

@ -5,6 +5,7 @@
#include <components/esm3/loadland.hpp>
#include <algorithm>
#include <iterator>
#include <random>
namespace
@ -24,29 +25,25 @@ namespace
PreparedNavMeshData mValue;
};
template <typename Random>
osg::Vec2i generateVec2i(int max, Random& random)
osg::Vec2i generateVec2i(int max, auto& random)
{
std::uniform_int_distribution<int> distribution(0, max);
return osg::Vec2i(distribution(random), distribution(random));
}
template <typename Random>
osg::Vec3f generateAgentHalfExtents(float min, float max, Random& random)
osg::Vec3f generateAgentHalfExtents(float min, float max, auto& random)
{
std::uniform_int_distribution<int> distribution(min, max);
return osg::Vec3f(distribution(random), distribution(random), distribution(random));
}
template <typename OutputIterator, typename Random>
void generateVertices(OutputIterator out, std::size_t number, Random& random)
void generateVertices(std::output_iterator<int> auto out, std::size_t number, auto& random)
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
std::generate_n(out, 3 * (number - number % 3), [&] { return distribution(random); });
}
template <typename OutputIterator, typename Random>
void generateIndices(OutputIterator out, int max, std::size_t number, Random& random)
void generateIndices(std::output_iterator<int> auto out, int max, std::size_t number, auto& random)
{
std::uniform_int_distribution<int> distribution(0, max);
std::generate_n(out, number - number % 3, [&] { return distribution(random); });
@ -70,21 +67,18 @@ namespace
return AreaType_null;
}
template <typename Random>
AreaType generateAreaType(Random& random)
AreaType generateAreaType(auto& random)
{
std::uniform_int_distribution<int> distribution(0, 4);
return toAreaType(distribution(random));
}
template <typename OutputIterator, typename Random>
void generateAreaTypes(OutputIterator out, std::size_t triangles, Random& random)
void generateAreaTypes(std::output_iterator<AreaType> auto out, std::size_t triangles, auto& random)
{
std::generate_n(out, triangles, [&] { return generateAreaType(random); });
}
template <typename OutputIterator, typename Random>
void generateWater(OutputIterator out, std::size_t count, Random& random)
void generateWater(std::output_iterator<CellWater> auto out, std::size_t count, auto& random)
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
std::generate_n(out, count, [&] {
@ -92,8 +86,7 @@ namespace
});
}
template <class Random>
Mesh generateMesh(std::size_t triangles, Random& random)
Mesh generateMesh(std::size_t triangles, auto& random)
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
std::vector<float> vertices;
@ -109,8 +102,7 @@ namespace
return Mesh(std::move(indices), std::move(vertices), std::move(areaTypes));
}
template <class Random>
Heightfield generateHeightfield(Random& random)
Heightfield generateHeightfield(auto& random)
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
Heightfield result;
@ -127,8 +119,7 @@ namespace
return result;
}
template <class Random>
FlatHeightfield generateFlatHeightfield(Random& random)
FlatHeightfield generateFlatHeightfield(auto& random)
{
std::uniform_real_distribution<float> distribution(0.0, 1.0);
FlatHeightfield result;
@ -138,8 +129,7 @@ namespace
return result;
}
template <class Random>
Key generateKey(std::size_t triangles, Random& random)
Key generateKey(std::size_t triangles, auto& random)
{
const CollisionShapeType agentShapeType = CollisionShapeType::Aabb;
const osg::Vec3f agentHalfExtents = generateAgentHalfExtents(0.5, 1.5, random);
@ -158,14 +148,12 @@ namespace
constexpr std::size_t trianglesPerTile = 239;
template <typename OutputIterator, typename Random>
void generateKeys(OutputIterator out, std::size_t count, Random& random)
void generateKeys(std::output_iterator<Key> auto out, std::size_t count, auto& random)
{
std::generate_n(out, count, [&] { return generateKey(trianglesPerTile, random); });
}
template <typename OutputIterator, typename Random>
void fillCache(OutputIterator out, Random& random, NavMeshTilesCache& cache)
void fillCache(std::output_iterator<Key> auto out, auto& random, NavMeshTilesCache& cache)
{
std::size_t size = cache.getStats().mNavMeshCacheSize;
@ -194,7 +182,7 @@ namespace
for (auto _ : state)
{
const auto& key = keys[n++ % keys.size()];
const auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh);
auto result = cache.get(key.mAgentBounds, key.mTilePosition, key.mRecastMesh);
benchmark::DoNotOptimize(result);
}
}
@ -253,7 +241,7 @@ namespace
while (state.KeepRunning())
{
const auto& key = keys[n++ % keys.size()];
const auto result = cache.set(
auto result = cache.set(
key.mAgentBounds, key.mTilePosition, key.mRecastMesh, std::make_unique<PreparedNavMeshData>());
benchmark::DoNotOptimize(result);
}

View file

@ -5,7 +5,7 @@ if (UNIX AND NOT APPLE)
target_link_libraries(openmw_esm_refid_benchmark ${CMAKE_THREAD_LIBS_INIT})
endif()
if (MSVC)
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(openmw_esm_refid_benchmark PRIVATE <algorithm>)
endif()

View file

@ -8,7 +8,7 @@ if (UNIX AND NOT APPLE)
target_link_libraries(openmw_settings_access_benchmark ${CMAKE_THREAD_LIBS_INIT})
endif()
if (MSVC)
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(openmw_settings_access_benchmark PRIVATE <algorithm>)
endif()

View file

@ -38,7 +38,7 @@ namespace
{
for (auto _ : state)
{
static const float v = Settings::Manager::getFloat("sky blending start", "Fog");
static float v = Settings::Manager::getFloat("sky blending start", "Fog");
benchmark::DoNotOptimize(v);
}
}
@ -47,8 +47,8 @@ namespace
{
for (auto _ : state)
{
static const float v1 = Settings::Manager::getFloat("near clip", "Camera");
static const bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing");
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);
}
@ -58,9 +58,9 @@ namespace
{
for (auto _ : state)
{
static const float v1 = Settings::Manager::getFloat("near clip", "Camera");
static const bool v2 = Settings::Manager::getBool("transparent postpass", "Post Processing");
static const int v3 = Settings::Manager::getInt("reflection detail", "Water");
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);
@ -71,7 +71,8 @@ namespace
{
for (auto _ : state)
{
benchmark::DoNotOptimize(Settings::fog().mSkyBlendingStart.get());
float v = Settings::fog().mSkyBlendingStart.get();
benchmark::DoNotOptimize(v);
}
}
@ -79,8 +80,10 @@ namespace
{
for (auto _ : state)
{
benchmark::DoNotOptimize(Settings::postProcessing().mTransparentPostpass.get());
benchmark::DoNotOptimize(Settings::camera().mNearClip.get());
bool v1 = Settings::postProcessing().mTransparentPostpass.get();
float v2 = Settings::camera().mNearClip.get();
benchmark::DoNotOptimize(v1);
benchmark::DoNotOptimize(v2);
}
}
@ -88,9 +91,12 @@ namespace
{
for (auto _ : state)
{
benchmark::DoNotOptimize(Settings::postProcessing().mTransparentPostpass.get());
benchmark::DoNotOptimize(Settings::camera().mNearClip.get());
benchmark::DoNotOptimize(Settings::water().mReflectionDetail.get());
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);
}
}

View file

@ -9,7 +9,7 @@ openmw_add_executable(bsatool
)
target_link_libraries(bsatool
${Boost_PROGRAM_OPTIONS_LIBRARY}
Boost::program_options
components
)
@ -18,7 +18,7 @@ if (BUILD_WITH_CODE_COVERAGE)
target_link_libraries(bsatool gcov)
endif()
if (MSVC)
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(bsatool PRIVATE
<filesystem>
<fstream>

View file

@ -194,7 +194,8 @@ int extract(std::unique_ptr<File>& bsa, Arguments& info)
// Get a stream for the file to extract
for (auto it = bsa->getList().rbegin(); it != bsa->getList().rend(); ++it)
{
if (Misc::StringUtils::ciEqual(Misc::StringUtils::stringToU8String(it->name()), archivePath))
auto streamPath = Misc::StringUtils::stringToU8String(it->name());
if (Misc::StringUtils::ciEqual(streamPath, archivePath) || Misc::StringUtils::ciEqual(streamPath, extractPath))
{
stream = bsa->getFile(&*it);
break;
@ -320,21 +321,27 @@ int main(int argc, char** argv)
// Open file
// TODO: add a version argument for this mode after compressed BSA writing is a thing
if (info.mode == "create")
return call<Bsa::BSAFile>(info);
Bsa::BsaVersion bsaVersion = Bsa::BSAFile::detectVersion(info.filename);
switch (bsaVersion)
{
case Bsa::BSAVER_COMPRESSED:
return call<Bsa::CompressedBSAFile>(info);
case Bsa::BSAVER_BA2_GNRL:
return call<Bsa::BA2GNRLFile>(info);
case Bsa::BSAVER_BA2_DX10:
return call<Bsa::BA2DX10File>(info);
case Bsa::BSAVER_UNCOMPRESSED:
case Bsa::BsaVersion::Unknown:
break;
case Bsa::BsaVersion::Uncompressed:
return call<Bsa::BSAFile>(info);
default:
throw std::runtime_error("Unrecognised BSA archive");
case Bsa::BsaVersion::Compressed:
return call<Bsa::CompressedBSAFile>(info);
case Bsa::BsaVersion::BA2GNRL:
return call<Bsa::BA2GNRLFile>(info);
case Bsa::BsaVersion::BA2DX10:
return call<Bsa::BA2DX10File>(info);
}
throw std::runtime_error("Unrecognised BSA archive");
}
catch (std::exception& e)
{

View file

@ -6,7 +6,7 @@ source_group(apps\\bulletobjecttool FILES ${BULLETMESHTOOL})
openmw_add_executable(openmw-bulletobjecttool ${BULLETMESHTOOL})
target_link_libraries(openmw-bulletobjecttool
${Boost_PROGRAM_OPTIONS_LIBRARY}
Boost::program_options
components
)
@ -19,7 +19,7 @@ if (WIN32)
install(TARGETS openmw-bulletobjecttool RUNTIME DESTINATION ".")
endif()
if (MSVC)
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(openmw-bulletobjecttool PRIVATE
<string>
<vector>

View file

@ -12,6 +12,7 @@
#include <components/files/multidircollection.hpp>
#include <components/misc/strings/conversion.hpp>
#include <components/platform/platform.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/bulletshape.hpp>
#include <components/resource/bulletshapemanager.hpp>
#include <components/resource/foreachbulletobject.hpp>
@ -76,10 +77,6 @@ namespace
bpo::value<StringsVector>()->default_value(StringsVector(), "fallback-archive")->multitoken()->composing(),
"set fallback BSA archives (later archives have higher priority)");
addOption("resources",
bpo::value<Files::MaybeQuotedPath>()->default_value(Files::MaybeQuotedPath(), "resources"),
"set resources directory");
addOption("content", bpo::value<StringsVector>()->default_value(StringsVector(), "")->multitoken()->composing(),
"content file(s): esm/esp, or omwgame/omwaddon/omwscripts");
@ -124,14 +121,14 @@ namespace
if (variables.find("help") != variables.end())
{
getRawStdout() << desc << std::endl;
Debug::getRawStdout() << desc << std::endl;
return 0;
}
Files::ConfigurationManager config;
config.readConfiguration(variables, desc);
setupLogging(config.getLogPath(), applicationName);
Debug::setupLogging(config.getLogPath(), applicationName);
const std::string encoding(variables["encoding"].as<std::string>());
Log(Debug::Info) << ToUTF8::encodingUsingMessage(encoding);
@ -145,13 +142,14 @@ namespace
config.filterOutNonExistingPaths(dataDirs);
const auto resDir = variables["resources"].as<Files::MaybeQuotedPath>();
const auto v = Version::getOpenmwVersion(resDir);
Log(Debug::Info) << v.describe();
const auto& resDir = variables["resources"].as<Files::MaybeQuotedPath>();
Log(Debug::Info) << Version::getOpenmwVersionDescription();
dataDirs.insert(dataDirs.begin(), resDir / "vfs");
const auto fileCollections = Files::Collections(dataDirs);
const auto archives = variables["fallback-archive"].as<StringsVector>();
const auto contentFiles = variables["content"].as<StringsVector>();
const Files::Collections fileCollections(dataDirs);
const auto& archives = variables["fallback-archive"].as<StringsVector>();
StringsVector contentFiles{ "builtin.omwscripts" };
const auto& configContentFiles = variables["content"].as<StringsVector>();
contentFiles.insert(contentFiles.end(), configContentFiles.begin(), configContentFiles.end());
Fallback::Map::init(variables["fallback"].as<Fallback::FallbackMap>().mMap);
@ -173,10 +171,12 @@ namespace
const EsmLoader::EsmData esmData
= EsmLoader::loadEsmData(query, contentFiles, fileCollections, readers, &encoder);
Resource::ImageManager imageManager(&vfs);
Resource::NifFileManager nifFileManager(&vfs);
Resource::SceneManager sceneManager(&vfs, &imageManager, &nifFileManager);
Resource::BulletShapeManager bulletShapeManager(&vfs, &sceneManager, &nifFileManager);
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) {
@ -202,5 +202,5 @@ namespace
int main(int argc, char* argv[])
{
return wrapApplication(runBulletObjectTool, argc, argv, applicationName);
return Debug::wrapApplication(runBulletObjectTool, argc, argv, applicationName);
}

View file

@ -2,19 +2,7 @@ include_directories(SYSTEM ${GTEST_INCLUDE_DIRS})
include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS})
file(GLOB UNITTEST_SRC_FILES
testing_util.hpp
../openmw/mwworld/store.cpp
../openmw/mwworld/esmstore.cpp
../openmw/mwworld/timestamp.cpp
mwworld/test_store.cpp
mwworld/testduration.cpp
mwworld/testtimestamp.cpp
mwdialogue/test_keywordsearch.cpp
mwscript/test_scripts.cpp
main.cpp
esm/test_fixed_string.cpp
esm/variant.cpp
@ -28,14 +16,17 @@ file(GLOB UNITTEST_SRC_FILES
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/test_stringops.cpp
misc/compression.cpp
misc/progressreporter.cpp
misc/test_endianness.cpp
misc/test_resourcehelpers.cpp
misc/progressreporter.cpp
misc/compression.cpp
misc/test_stringops.cpp
misc/testmathutil.cpp
nifloader/testbulletnifloader.cpp
@ -64,9 +55,6 @@ file(GLOB UNITTEST_SRC_FILES
shader/parselinks.cpp
shader/shadermanager.cpp
../openmw/options.cpp
openmw/options.cpp
sqlite3/db.cpp
sqlite3/request.cpp
sqlite3/statement.cpp
@ -90,25 +78,37 @@ file(GLOB UNITTEST_SRC_FILES
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\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
source_group(apps\\components-tests FILES ${UNITTEST_SRC_FILES})
openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
openmw_add_executable(components-tests ${UNITTEST_SRC_FILES})
target_link_libraries(components-tests
GTest::GTest
GMock::GMock
components
)
target_link_libraries(openmw_test_suite 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(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(components-tests ${CMAKE_THREAD_LIBS_INIT})
endif()
if (BUILD_WITH_CODE_COVERAGE)
target_compile_options(openmw_test_suite PRIVATE --coverage)
target_link_libraries(openmw_test_suite gcov)
target_compile_options(components-tests PRIVATE --coverage)
target_link_libraries(components-tests gcov)
endif()
file(DOWNLOAD
@ -117,12 +117,12 @@ file(DOWNLOAD
EXPECTED_HASH SHA512=6e38642bcf013c5f496a9cb0bf3ec7c9553b6e86b836e7844824c5a05f556c9391167214469b6318401684b702d7569896bf743c85aee4198612b3315ba778d6
)
target_compile_definitions(openmw_test_suite
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)
target_precompile_headers(openmw_test_suite PRIVATE
if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
target_precompile_headers(components-tests PRIVATE
<boost/program_options/options_description.hpp>
<gtest/gtest.h>

View file

@ -33,7 +33,8 @@ namespace
{
const ObjectId id(&shape);
osg::ref_ptr<Resource::BulletShape> bulletShape(new Resource::BulletShape);
bulletShape->mFileName = "test.nif";
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);
@ -52,7 +53,7 @@ namespace
OffMeshConnectionsManager mOffMeshConnectionsManager{ mSettings.mRecast };
const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, { 29, 29, 66 } };
const TilePosition mPlayerTile{ 0, 0 };
const std::string mWorldspace = "sys::default";
const ESM::RefId mWorldspace = ESM::RefId::stringRefId("sys::default");
const btBoxShape mBox{ btVector3(100, 100, 20) };
Loading::Listener mListener;
};
@ -266,20 +267,11 @@ namespace
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
updater.stop();
const std::set<TilePosition> present{
TilePosition(-2, 0),
TilePosition(-1, -1),
TilePosition(-1, 0),
TilePosition(-1, 1),
TilePosition(0, -2),
TilePosition(0, -1),
TilePosition(0, 0),
TilePosition(0, 1),
TilePosition(0, 2),
TilePosition(1, -1),
TilePosition(1, 0),
};
std::size_t present = 0;
for (int x = -5; x <= 5; ++x)
{
for (int y = -5; y <= 5; ++y)
{
const TilePosition tilePosition(x, y);
@ -289,14 +281,323 @@ namespace
recastMesh->getMeshSources(), [&](const MeshSource& v) { return resolveMeshSource(*dbPtr, v); });
if (std::holds_alternative<MeshSource>(objects))
continue;
EXPECT_EQ(dbPtr
present += dbPtr
->findTile(mWorldspace, tilePosition,
serialize(mSettings.mRecast, mAgentBounds, *recastMesh,
std::get<std::vector<DbRefGeometryObject>>(objects)))
.has_value(),
present.find(tilePosition) != present.end())
<< tilePosition.x() << " " << tilePosition.y()
<< " present=" << (present.find(tilePosition) != present.end());
.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<NavMeshDb>(":memory:", std::numeric_limits<std::uint64_t>::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<GuardedNavMeshCacheItem>(1, mSettings);
const TilePosition tilePosition{ 0, 0 };
const std::map<TilePosition, ChangeType> 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<DbRefGeometryObject> 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<GuardedNavMeshCacheItem>(1, mSettings);
std::map<TilePosition, ChangeType> changedTiles;
for (int x = -3; x <= 3; ++x)
for (int y = -3; y <= 3; ++y)
changedTiles.emplace(TilePosition{ x, y }, ChangeType::update);
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
updater.wait(WaitConditionType::allJobsDone, &mListener);
{
const AsyncNavMeshUpdaterStats stats = updater.getStats();
EXPECT_EQ(stats.mJobs, 0);
EXPECT_EQ(stats.mWaiting.mDelayed, 0);
}
updater.post(mAgentBounds, navMeshCacheItem, mPlayerTile, mWorldspace, changedTiles);
{
const AsyncNavMeshUpdaterStats stats = updater.getStats();
EXPECT_EQ(stats.mJobs, 49);
EXPECT_EQ(stats.mWaiting.mDelayed, 49);
}
updater.wait(WaitConditionType::allJobsDone, &mListener);
{
const AsyncNavMeshUpdaterStats stats = updater.getStats();
EXPECT_EQ(stats.mJobs, 0);
EXPECT_EQ(stats.mWaiting.mDelayed, 0);
}
}
struct DetourNavigatorSpatialJobQueueTest : Test
{
const AgentBounds mAgentBounds{ CollisionShapeType::Aabb, osg::Vec3f(1, 1, 1) };
const std::shared_ptr<GuardedNavMeshCacheItem> mNavMeshCacheItemPtr;
const std::weak_ptr<GuardedNavMeshCacheItem> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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<Job> 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);
}
}

View file

@ -0,0 +1,165 @@
#include <components/detournavigator/debug.hpp>
#include <components/detournavigator/gettilespositions.hpp>
#include <components/detournavigator/settings.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
namespace
{
using namespace testing;
using namespace DetourNavigator;
struct CollectTilesPositions
{
std::vector<TilePosition>& mTilesPositions;
void operator()(const TilePosition& value) { mTilesPositions.push_back(value); }
};
struct DetourNavigatorGetTilesPositionsTest : Test
{
RecastSettings mSettings;
std::vector<TilePosition> 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<TilesPositionsRangeParams>
{
};
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<TilesPositionsRangeParams>
{
};
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));
}

View file

@ -39,37 +39,44 @@ namespace
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<Navigator> mNavigator;
const osg::Vec3f mPlayerPosition;
const std::string mWorldspace;
std::unique_ptr<Navigator> mNavigator = std::make_unique<NavigatorImpl>(
mSettings, std::make_unique<NavMeshDb>(":memory:", std::numeric_limits<std::uint64_t>::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;
osg::Vec3f mEnd;
osg::Vec3f mStart{ 52, 460, 1 };
osg::Vec3f mEnd{ 460, 52, 1 };
std::deque<osg::Vec3f> mPath;
std::back_insert_iterator<std::deque<osg::Vec3f>> mOut;
std::back_insert_iterator<std::deque<osg::Vec3f>> mOut{ mPath };
AreaCosts mAreaCosts;
Loading::Listener mListener;
const osg::Vec2i mCellPosition{ 0, 0 };
const int mHeightfieldTileSize = ESM::Land::REAL_SIZE / (ESM::Land::LAND_SIZE - 1);
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 };
DetourNavigatorNavigatorTest()
: mPlayerPosition(256, 256, 0)
, mWorldspace("sys::default")
, mStart(52, 460, 1)
, mEnd(460, 52, 1)
, mOut(mPath)
{
mNavigator.reset(new NavigatorImpl(
mSettings, std::make_unique<NavMeshDb>(":memory:", std::numeric_limits<std::uint64_t>::max())));
}
};
constexpr std::array<float, 5 * 5> 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<btScalar, 5 * 5> 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::size_t size>
std::unique_ptr<btHeightfieldTerrainShape> makeSquareHeightfieldTerrainShape(
const std::array<btScalar, size>& values, btScalar heightScale = 1, int upAxis = 2,
@ -113,7 +120,7 @@ namespace
{
}
T& shape() { return static_cast<T&>(*mInstance->mCollisionShape); }
T& shape() const { return static_cast<T&>(*mInstance->mCollisionShape); }
const osg::ref_ptr<const Resource::BulletShapeInstance>& instance() const { return mInstance; }
private:
@ -150,15 +157,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, update_then_find_path_should_return_path)
{
constexpr std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
auto updateGuard = mNavigator->makeUpdateGuard();
@ -177,21 +177,32 @@ namespace
<< 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<int>(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<NavMeshDb>(":memory:", std::numeric_limits<std::uint64_t>::max())));
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(
@ -235,15 +246,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, update_changed_object_should_change_navmesh)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(
@ -288,14 +292,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, for_overlapping_heightfields_objects_should_use_higher)
{
const std::array<btScalar, 5 * 5> heightfieldData1{ {
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
} };
CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(heightfieldData1));
CollisionShapeInstance heightfield1(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar));
heightfield1.shape().setLocalScaling(btVector3(128, 128, 1));
const std::array<btScalar, 5 * 5> heightfieldData2{ {
@ -328,15 +325,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, only_one_heightfield_per_cell_is_allowed)
{
const std::array<float, 5 * 5> heightfieldData1{ {
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
} };
const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(heightfieldData1);
const int cellSize1 = mHeightfieldTileSize * (surface1.mSize - 1);
const HeightfieldSurface surface1 = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize1 = heightfieldTileSize * (surface1.mSize - 1);
const std::array<float, 5 * 5> heightfieldData2{ {
-25, -25, -25, -25, -25, // row 0
@ -346,7 +336,7 @@ namespace
-25, -25, -25, -25, -25, // row 4
} };
const HeightfieldSurface surface2 = makeSquareHeightfieldSurface(heightfieldData2);
const int cellSize2 = mHeightfieldTileSize * (surface2.mSize - 1);
const int cellSize2 = heightfieldTileSize * (surface2.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize1, surface1, nullptr);
@ -366,14 +356,8 @@ namespace
{
osg::ref_ptr<Resource::BulletShape> bulletShape(new Resource::BulletShape);
std::array<btScalar, 5 * 5> heightfieldData{ {
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
} };
std::unique_ptr<btHeightfieldTerrainShape> shapePtr = makeSquareHeightfieldTerrainShape(heightfieldData);
std::unique_ptr<btHeightfieldTerrainShape> shapePtr
= makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar);
shapePtr->setLocalScaling(btVector3(128, 128, 1));
bulletShape->mCollisionShape.reset(shapePtr.release());
@ -419,7 +403,7 @@ namespace
0, -50, -100, -100, -100, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addWater(mCellPosition, cellSize, 300, nullptr);
@ -453,7 +437,7 @@ namespace
0, 0, 0, 0, 0, 0, 0, // row 6
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addWater(mCellPosition, cellSize, -25, nullptr);
@ -487,7 +471,7 @@ namespace
0, 0, 0, 0, 0, 0, 0, // row 6
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
@ -520,7 +504,7 @@ namespace
0, 0, 0, 0, 0, 0, 0, // row 6
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addWater(mCellPosition, cellSize, -25, nullptr);
@ -542,14 +526,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, update_object_remove_and_update_then_find_path_should_return_path)
{
const std::array<btScalar, 5 * 5> heightfieldData{ {
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
} };
CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(heightfieldData));
CollisionShapeInstance heightfield(makeSquareHeightfieldTerrainShape(defaultHeightfieldDataScalar));
heightfield.shape().setLocalScaling(btVector3(128, 128, 1));
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
@ -579,15 +556,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, update_heightfield_remove_and_update_then_find_path_should_return_path)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
@ -623,7 +593,7 @@ namespace
0, -25, -100, -100, -100, -100, // row 5
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
@ -649,15 +619,8 @@ namespace
mNavigator.reset(new NavigatorImpl(
mSettings, std::make_unique<NavMeshDb>(":memory:", std::numeric_limits<std::uint64_t>::max())));
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
const btVector3 shift = getHeightfieldShift(mCellPosition, cellSize, surface.mMinHeight, surface.mMaxHeight);
std::vector<CollisionShapeInstance<btBoxShape>> boxes;
@ -745,15 +708,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, update_then_raycast_should_return_position)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
@ -771,15 +727,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest,
update_for_oscillating_object_that_does_not_change_navmesh_should_not_trigger_navmesh_update)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
CollisionShapeInstance oscillatingBox(std::make_unique<btBoxShape>(btVector3(20, 20, 20)));
const btVector3 oscillatingBoxShapePosition(288, 288, 400);
@ -819,7 +768,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, should_provide_path_over_flat_heightfield)
{
const HeightfieldPlane plane{ 100 };
const int cellSize = mHeightfieldTileSize * 4;
const int cellSize = heightfieldTileSize * 4;
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, plane, nullptr);
@ -837,15 +786,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, for_not_reachable_destination_find_path_should_provide_partial_path)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)),
@ -870,15 +812,8 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, end_tolerance_should_extent_available_destinations)
{
const std::array<float, 5 * 5> heightfieldData{ {
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
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = mHeightfieldTileSize * (surface.mSize - 1);
const HeightfieldSurface surface = makeSquareHeightfieldSurface(defaultHeightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
CollisionShapeInstance compound(std::make_unique<btCompoundShape>());
compound.shape().addChildShape(btTransform(btMatrix3x3::getIdentity(), btVector3(204, -204, 0)),
@ -924,16 +859,29 @@ namespace
EXPECT_EQ(mNavigator->getNavMesh(mAgentBounds)->lockConst()->getVersion(), version);
}
std::pair<TilePosition, TilePosition> 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<float>(2 * static_cast<std::int64_t>(std::numeric_limits<int>::max()) - 1);
const float size = static_cast<float>((1 << 22) - 1);
CollisionShapeInstance bigBox(std::make_unique<btBoxShape>(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> cellGridBounds = std::nullopt;
const osg::Vec3f playerPosition(32, 1024, 0);
mNavigator->updateBounds(mPlayerPosition, nullptr);
mNavigator->updateBounds(mWorldspace, cellGridBounds, playerPosition, nullptr);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addObject(ObjectId(&bigBox.shape()), ObjectShapes(bigBox.instance(), objectTransform),
btTransform::getIdentity(), nullptr);
@ -943,7 +891,8 @@ namespace
std::mutex mutex;
std::thread thread([&] {
mNavigator->update(mPlayerPosition, nullptr);
auto guard = mNavigator->makeUpdateGuard();
mNavigator->update(playerPosition, guard.get());
std::lock_guard lock(mutex);
updated = true;
updateFinished.notify_all();
@ -951,7 +900,7 @@ namespace
{
std::unique_lock lock(mutex);
updateFinished.wait_for(lock, std::chrono::seconds(3), [&] { return updated; });
updateFinished.wait_for(lock, std::chrono::seconds(10), [&] { return updated; });
ASSERT_TRUE(updated);
}
@ -959,14 +908,69 @@ namespace
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(mNavigator->getRecastMeshTiles().size(), 509);
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, 509);
EXPECT_EQ(usedNavMeshTiles, 1024);
}
TEST_F(DetourNavigatorNavigatorTest, update_should_be_limited_by_cell_grid_bounds)
{
const float size = static_cast<float>((1 << 22) - 1);
CollisionShapeInstance bigBox(std::make_unique<btBoxShape>(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<AgentBounds>
@ -1000,4 +1004,286 @@ namespace
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<int>(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<int>(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<int>(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::function<void(Navigator&)>>
{
};
std::vector<TilePosition> getUsedTiles(const NavMeshCacheItem& navMesh)
{
std::vector<TilePosition> 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<NavMeshDb>(":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<int>(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<btBoxShape> mBox{ std::make_unique<btBoxShape>(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<void(Navigator&)> addNavMeshData[] = {
AddHeightfieldSurface{},
AddHeightfieldPlane{},
AddWater{},
AddObject{},
AddAll{},
};
INSTANTIATE_TEST_SUITE_P(DifferentNavMeshData, DetourNavigatorUpdateTest, ValuesIn(addNavMeshData));
}

View file

@ -8,7 +8,6 @@
#include <gtest/gtest.h>
#include <limits>
#include <numeric>
#include <random>
namespace
@ -19,7 +18,7 @@ namespace
struct Tile
{
std::string mWorldspace;
ESM::RefId mWorldspace;
TilePosition mTilePosition;
std::vector<std::byte> mInput;
std::vector<std::byte> mData;
@ -39,12 +38,12 @@ namespace
Tile insertTile(TileId tileId, TileVersion version)
{
std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const TilePosition tilePosition{ 3, 4 };
std::vector<std::byte> input = generateData();
std::vector<std::byte> data = generateData();
EXPECT_EQ(mDb.insertTile(tileId, worldspace, tilePosition, version, input, data), 1);
return { std::move(worldspace), tilePosition, std::move(input), std::move(data) };
return { worldspace, tilePosition, std::move(input), std::move(data) };
}
};
@ -89,7 +88,7 @@ namespace
{
const TileId tileId{ 53 };
const TileVersion version{ 1 };
const std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const TilePosition tilePosition{ 3, 4 };
const std::vector<std::byte> input = generateData();
const std::vector<std::byte> data = generateData();
@ -101,7 +100,7 @@ namespace
{
const TileId tileId{ 53 };
const TileVersion version{ 1 };
const std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const TilePosition tilePosition{ 3, 4 };
const std::vector<std::byte> input = generateData();
const std::vector<std::byte> data = generateData();
@ -113,7 +112,7 @@ namespace
TEST_F(DetourNavigatorNavMeshDbTest, delete_tiles_at_should_remove_all_tiles_with_given_worldspace_and_position)
{
const TileVersion version{ 1 };
const std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const TilePosition tilePosition{ 3, 4 };
const std::vector<std::byte> input1 = generateData();
const std::vector<std::byte> input2 = generateData();
@ -130,7 +129,7 @@ namespace
const TileId leftTileId{ 53 };
const TileId removedTileId{ 54 };
const TileVersion version{ 1 };
const std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const TilePosition tilePosition{ 3, 4 };
const std::vector<std::byte> leftInput = generateData();
const std::vector<std::byte> removedInput = generateData();
@ -148,7 +147,7 @@ namespace
{
TileId tileId{ 1 };
const TileVersion version{ 1 };
const std::string worldspace = "sys::default";
const ESM::RefId worldspace = ESM::RefId::stringRefId("sys::default");
const std::vector<std::byte> input = generateData();
const std::vector<std::byte> data = generateData();
for (int x = -2; x <= 2; ++x)

View file

@ -42,12 +42,24 @@ namespace testing
<< ", " << 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<osg::Vec3f>& value)
{
return (*this) << value.mValue;
}
template <>
inline testing::Message& Message::operator<<(const Wrapper<osg::Vec2i>& value)
{
return (*this) << value.mValue;
}
template <>
inline testing::Message& Message::operator<<(const Wrapper<float>& value)
{
@ -72,6 +84,12 @@ namespace testing
return writeRange(*this, value, 1);
}
template <>
inline testing::Message& Message::operator<<(const std::vector<osg::Vec2i>& value)
{
return writeRange(*this, value, 1);
}
template <>
inline testing::Message& Message::operator<<(const std::vector<float>& value)
{

View file

@ -39,7 +39,7 @@ namespace DetourNavigator
result.mDetour.mMaxPolygonPathSize = 1024;
result.mDetour.mMaxSmoothPathSize = 1024;
result.mDetour.mMaxPolys = 4096;
result.mMaxTilesNumber = 512;
result.mMaxTilesNumber = 1024;
result.mMinUpdateInterval = std::chrono::milliseconds(50);
result.mWriteToNavMeshDb = true;
return result;

View file

@ -21,6 +21,7 @@ namespace
const ObjectTransform mObjectTransform{ ESM::Position{ { 0, 0, 0 }, { 0, 0, 0 } }, 0.0f };
const osg::ref_ptr<const Resource::BulletShape> mShape = new Resource::BulletShape;
const osg::ref_ptr<const Resource::BulletShapeInstance> mInstance = new Resource::BulletShapeInstance(mShape);
const ESM::RefId mWorldspace = ESM::RefId::stringRefId("worldspace");
DetourNavigatorTileCachedRecastMeshManagerTest()
{
@ -34,7 +35,7 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_mesh_for_empty_should_return_nullptr)
{
TileCachedRecastMeshManager manager(mSettings);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(0, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, get_revision_for_empty_should_return_zero)
@ -65,14 +66,14 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_add_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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("worldspace", TilePosition(x, y)), nullptr);
ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_object_should_return_add_changed_tiles)
@ -145,25 +146,25 @@ namespace
get_mesh_after_add_object_should_return_recast_mesh_for_each_used_tile)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), 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("worldspace", nullptr);
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("worldspace", TilePosition(1, 0)), nullptr);
EXPECT_EQ(manager.getMesh(mWorldspace, TilePosition(1, 0)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest,
@ -172,10 +173,10 @@ namespace
TileCachedRecastMeshManager manager(mSettings);
const TilesPositionsRange range{
.mBegin = TilePosition(-1, -1),
.mEnd = TilePosition(1, 1),
.mEnd = TilePosition(2, 2),
};
manager.setRange(range, nullptr);
manager.setWorldspace("worldspace", nullptr);
manager.setWorldspace(mWorldspace, nullptr);
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(
@ -183,23 +184,23 @@ namespace
const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(1, -1)), 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("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), 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("worldspace", nullptr);
manager.setWorldspace(mWorldspace, nullptr);
const btBoxShape boxShape(btVector3(20, 20, 100));
const btTransform transform(
@ -207,48 +208,48 @@ namespace
const CollisionShape shape(mInstance, boxShape, mObjectTransform);
manager.addObject(ObjectId(&boxShape), shape, transform, AreaType::AreaType_ground, nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), 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("worldspace", TilePosition(1, 0)), nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(1, -1)), 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("worldspace", nullptr);
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("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_EQ(manager.getMesh("worldspace", TilePosition(0, 0)), 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("worldspace", nullptr);
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("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), 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("worldspace", TilePosition(-1, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(-1, 0)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, -1)), nullptr);
EXPECT_NE(manager.getMesh("worldspace", TilePosition(0, 0)), 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,
@ -293,7 +294,7 @@ namespace
get_revision_after_update_not_changed_object_should_return_same_value)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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);
@ -339,19 +340,19 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, add_water_for_not_max_int_should_add_new_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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("worldspace", TilePosition(x, y)), nullptr);
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("worldspace", nullptr);
manager.setWorldspace(mWorldspace, nullptr);
const btBoxShape boxShape(btVector3(20, 20, 100));
const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(
@ -361,7 +362,7 @@ namespace
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("worldspace", TilePosition(x, y)) != nullptr,
ASSERT_EQ(manager.getMesh(mWorldspace, TilePosition(x, y)) != nullptr,
-1 <= x && x <= 0 && -1 <= y && y <= 0);
}
@ -390,20 +391,20 @@ namespace
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, remove_water_for_existing_cell_should_remove_empty_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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("worldspace", TilePosition(x, y)), nullptr);
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("worldspace", nullptr);
manager.setWorldspace(mWorldspace, nullptr);
const btBoxShape boxShape(btVector3(20, 20, 100));
const CollisionShape shape(mInstance, boxShape, mObjectTransform);
ASSERT_TRUE(manager.addObject(
@ -414,14 +415,14 @@ namespace
manager.removeWater(cellPosition, nullptr);
for (int x = -6; x < 6; ++x)
for (int y = -6; y < 6; ++y)
ASSERT_EQ(manager.getMesh("worldspace", TilePosition(x, y)) != nullptr,
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("worldspace", nullptr);
manager.setWorldspace(mWorldspace, nullptr);
const osg::Vec2i cellPosition(0, 0);
const int cellSize = 8192;
const btBoxShape boxShape(btVector3(20, 20, 100));
@ -432,24 +433,25 @@ namespace
manager.removeObject(ObjectId(&boxShape), nullptr);
for (int x = -1; x < 12; ++x)
for (int y = -1; y < 12; ++y)
ASSERT_NE(manager.getMesh("worldspace", TilePosition(x, y)), nullptr);
ASSERT_NE(manager.getMesh(mWorldspace, TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_new_worldspace_should_remove_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
manager.setWorldspace("worldspace", nullptr);
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));
manager.setWorldspace("other", 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("other", TilePosition(x, y)), nullptr);
ASSERT_EQ(manager.getMesh(otherWorldspace, TilePosition(x, y)), nullptr);
}
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_bounds_should_add_changed_tiles)
TEST_F(DetourNavigatorTileCachedRecastMeshManagerTest, set_range_should_add_changed_tiles)
{
TileCachedRecastMeshManager manager(mSettings);
const btBoxShape boxShape(btVector3(20, 20, 100));
@ -470,4 +472,35 @@ namespace
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);
}
}

View file

@ -1,6 +1,7 @@
#include "components/esm/refid.hpp"
#include "components/esm3/esmreader.hpp"
#include "components/esm3/esmwriter.hpp"
#include <components/esm/refid.hpp>
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/testing/expecterror.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@ -9,8 +10,6 @@
#include <map>
#include <string>
#include "../testing_util.hpp"
MATCHER(IsPrint, "")
{
return std::isprint(arg) != 0;

View file

@ -0,0 +1,57 @@
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/loaddial.hpp>
#include <gtest/gtest.h>
namespace ESM
{
namespace
{
TEST(Esm3CStringIdTest, dialNameShouldBeNullTerminated)
{
std::unique_ptr<std::istream> stream;
{
auto ostream = std::make_unique<std::stringstream>();
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);
}
}
}

View file

@ -57,7 +57,7 @@ namespace ESM
// If this test failed probably there is a change in RefId format and CurrentSaveGameFormatVersion should be
// incremented, current version should be handled.
TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNubmerOfBytes)
TEST_P(Esm3EsmWriterRefIdSizeTest, writeHRefIdShouldProduceCertainNumberOfBytes)
{
const auto [refId, size] = GetParam();

View file

@ -0,0 +1,762 @@
#include <components/esm/fourcc.hpp>
#include <components/esm3/aipackage.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/esm3/effectlist.hpp>
#include <components/esm3/esmreader.hpp>
#include <components/esm3/esmwriter.hpp>
#include <components/esm3/loadcont.hpp>
#include <components/esm3/loaddial.hpp>
#include <components/esm3/loadinfo.hpp>
#include <components/esm3/loadland.hpp>
#include <components/esm3/loadregn.hpp>
#include <components/esm3/loadscpt.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/esm3/player.hpp>
#include <components/esm3/quickkeys.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <iterator>
#include <limits>
#include <memory>
#include <numeric>
#include <random>
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<std::uint32_t>(value.mType)
<< "', .mId = " << value.mId << "}";
}
namespace
{
using namespace ::testing;
std::vector<ESM::FormatVersion> getFormats()
{
std::vector<ESM::FormatVersion> 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 <class T>
concept HasSave = requires(T v, ESMWriter& w)
{
v.save(w);
};
template <class T>
concept NotHasSave = !HasSave<T>;
template <HasSave T>
auto save(const T& record, ESMWriter& writer)
{
record.save(writer);
}
void save(const CellRef& record, ESMWriter& writer)
{
record.save(writer, true);
}
template <NotHasSave T>
auto save(const T& record, ESMWriter& writer)
{
writer.writeComposite(record);
}
template <typename T>
std::unique_ptr<std::istream> makeEsmStream(const T& record, FormatVersion formatVersion)
{
ESMWriter writer;
auto stream = std::make_unique<std::stringstream>();
writer.setFormatVersion(formatVersion);
writer.save(*stream);
writer.startRecord(fakeRecordId);
save(record, writer);
writer.endRecord(fakeRecordId);
return stream;
}
template <class T>
concept HasLoad = requires(T v, ESMReader& r)
{
v.load(r);
};
template <class T>
concept HasLoadWithDelete = requires(T v, ESMReader& r, bool& d)
{
v.load(r, d);
};
template <class T>
concept NotHasLoad = !HasLoad<T> && !HasLoadWithDelete<T>;
template <HasLoad T>
void load(ESMReader& reader, T& record)
{
record.load(reader);
}
template <HasLoadWithDelete T>
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 <NotHasLoad T>
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<LandRecordData>();
reader.restoreContext(record.mContext);
loadLandRecordData(record.mDataTypes, reader, *record.mLandData);
}
template <typename T>
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<FormatVersion>
{
std::minstd_rand mRandom;
std::uniform_int_distribution<short> mRefIdDistribution{ 'a', 'z' };
std::string generateRandomString(std::size_t size)
{
std::string value;
while (value.size() < size)
value.push_back(static_cast<char>(mRefIdDistribution(mRandom)));
return value;
}
RefId generateRandomRefId(std::size_t size = 33) { return RefId::stringRefId(generateRandomString(size)); }
template <class T, std::size_t n>
void generateArray(T (&dst)[n])
{
for (auto& v : dst)
v = std::uniform_real_distribution<float>{ -1.0f, 1.0f }(mRandom);
}
void generateBytes(auto iterator, std::size_t count)
{
std::uniform_int_distribution<unsigned short> distribution{ 0,
std::numeric_limits<unsigned char>::max() };
std::generate_n(iterator, count, [&] { return static_cast<unsigned char>(distribution(mRandom)); });
}
void generateStrings(auto iterator, std::size_t count)
{
std::uniform_int_distribution<std::size_t> 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<std::stringstream>();
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<unsigned>::max();
record.mRefNum.mContentFile = std::numeric_limits<int>::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<int>::max();
record.mChargeInt = std::numeric_limits<int>::max();
record.mEnchantmentCharge = std::numeric_limits<float>::max();
record.mCount = std::numeric_limits<int>::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<signed char>::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<LandRecordData>(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()));
}
}

View file

@ -1,5 +1,7 @@
#include <components/files/constrainedfilestream.hpp>
#include <components/files/conversion.hpp>
#include <components/files/hash.hpp>
#include <components/testing/util.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@ -10,8 +12,6 @@
#include <sstream>
#include <string>
#include "../testing_util.hpp"
namespace
{
using namespace testing;
@ -35,7 +35,8 @@ namespace
std::fill_n(std::back_inserter(content), 1, 'a');
std::istringstream stream(content);
stream.exceptions(std::ios::failbit | std::ios::badbit);
EXPECT_THAT(getHash(fileName, stream), ElementsAre(9607679276477937801ull, 16624257681780017498ull));
EXPECT_THAT(getHash(Files::pathToUnicodeString(fileName), stream),
ElementsAre(9607679276477937801ull, 16624257681780017498ull));
}
TEST_P(FilesGetHash, shouldReturnHashForStringStream)
@ -44,7 +45,7 @@ namespace
std::string content;
std::fill_n(std::back_inserter(content), GetParam().mSize, 'a');
std::istringstream stream(content);
EXPECT_EQ(getHash(fileName, stream), GetParam().mHash);
EXPECT_EQ(getHash(Files::pathToUnicodeString(fileName), stream), GetParam().mHash);
}
TEST_P(FilesGetHash, shouldReturnHashForConstrainedFileStream)
@ -57,7 +58,7 @@ namespace
std::fstream(file, std::ios_base::out | std::ios_base::binary)
.write(content.data(), static_cast<std::streamsize>(content.size()));
const auto stream = Files::openConstrainedFileStream(file, 0, content.size());
EXPECT_EQ(getHash(file, *stream), GetParam().mHash);
EXPECT_EQ(getHash(Files::pathToUnicodeString(file), *stream), GetParam().mHash);
}
INSTANTIATE_TEST_SUITE_P(Params, FilesGetHash,

View file

@ -1,17 +1,17 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/files/configurationmanager.hpp>
#include <components/fx/technique.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/settings/settings.hpp>
#include "../testing_util.hpp"
#include <components/testing/util.hpp>
namespace
{
constexpr VFS::Path::NormalizedView techniquePropertiesPath("shaders/technique_properties.omwfx");
TestingOpenMW::VFSTestFile technique_properties(R"(
TestingOpenMW::VFSTestFile techniqueProperties(R"(
fragment main {}
vertex main {}
technique {
@ -27,7 +27,9 @@ namespace
}
)");
TestingOpenMW::VFSTestFile rendertarget_properties{ R"(
constexpr VFS::Path::NormalizedView rendertargetPropertiesPath("shaders/rendertarget_properties.omwfx");
TestingOpenMW::VFSTestFile rendertargetProperties{ R"(
render_target rendertarget {
width_ratio = 0.5;
height_ratio = 0.5;
@ -53,7 +55,9 @@ namespace
technique { passes = downsample2x, main; }
)" };
TestingOpenMW::VFSTestFile uniform_properties{ R"(
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);
@ -67,13 +71,17 @@ namespace
technique { passes = main; }
)" };
TestingOpenMW::VFSTestFile missing_sampler_source{ R"(
constexpr VFS::Path::NormalizedView missingSamplerSourcePath("shaders/missing_sampler_source.omwfx");
TestingOpenMW::VFSTestFile missingSamplerSource{ R"(
sampler_1d mysampler1d { }
fragment main { }
technique { passes = main; }
)" };
TestingOpenMW::VFSTestFile repeated_shared_block{ R"(
constexpr VFS::Path::NormalizedView repeatedSharedBlockPath("shaders/repeated_shared_block.omwfx");
TestingOpenMW::VFSTestFile repeatedSharedBlock{ R"(
shared {
float myfloat = 1.0;
}
@ -93,13 +101,13 @@ namespace
TechniqueTest()
: mVFS(TestingOpenMW::createTestVFS({
{ "shaders/technique_properties.omwfx", &technique_properties },
{ "shaders/rendertarget_properties.omwfx", &rendertarget_properties },
{ "shaders/uniform_properties.omwfx", &uniform_properties },
{ "shaders/missing_sampler_source.omwfx", &missing_sampler_source },
{ "shaders/repeated_shared_block.omwfx", &repeated_shared_block },
{ techniquePropertiesPath, &techniqueProperties },
{ rendertargetPropertiesPath, &rendertargetProperties },
{ uniformPropertiesPath, &uniformProperties },
{ missingSamplerSourcePath, &missingSamplerSource },
{ repeatedSharedBlockPath, &repeatedSharedBlock },
}))
, mImageManager(mVFS.get())
, mImageManager(mVFS.get(), 0)
{
}

View file

@ -0,0 +1,57 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/lua/asyncpackage.hpp>
#include <components/lua/luastate.hpp>
#include <components/testing/expecterror.hpp>
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");
}
}

View file

@ -1,4 +1,4 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <fstream>
@ -9,8 +9,8 @@
#include <components/esm3/readerscache.hpp>
#include <components/lua/configuration.hpp>
#include <components/lua/serialization.hpp>
#include "../testing_util.hpp"
#include <components/testing/expecterror.hpp>
#include <components/testing/util.hpp>
namespace
{
@ -44,7 +44,7 @@ namespace
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[4]), ": my_mod/player.lua");
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CUSTOM CREATURE : my_mod/creature.lua");
LuaUtil::ScriptsConfiguration conf;
@ -54,7 +54,7 @@ namespace
// 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[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, "")));
@ -89,7 +89,7 @@ namespace
{
ESM::LuaScriptsCfg cfg;
ESM::LuaScriptCfg& script1 = cfg.mScripts.emplace_back();
script1.mScriptPath = "Script1.lua";
script1.mScriptPath = VFS::Path::Normalized("Script1.lua");
script1.mInitializationData = "data1";
script1.mFlags = ESM::LuaScriptCfg::sPlayer;
script1.mTypes.push_back(ESM::REC_CREA);
@ -98,12 +98,12 @@ namespace
script1.mRefs.push_back({ true, 2, 4, "" });
ESM::LuaScriptCfg& script2 = cfg.mScripts.emplace_back();
script2.mScriptPath = "Script2.lua";
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 = "script1.LUA";
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"), "" });
@ -115,8 +115,8 @@ namespace
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");
"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())),
@ -139,7 +139,7 @@ namespace
ElementsAre(Pair(0, "data1"), Pair(1, "")));
ESM::LuaScriptCfg& script3 = cfg.mScripts.emplace_back();
script3.mScriptPath = "script1.lua";
script3.mScriptPath = VFS::Path::Normalized("script1.lua");
script3.mFlags = ESM::LuaScriptCfg::sGlobal;
EXPECT_ERROR(conf.init(cfg), "Flags mismatch for script1.lua");
}
@ -168,13 +168,13 @@ namespace
}
{
ESM::LuaScriptCfg& script = cfg.mScripts.emplace_back();
script.mScriptPath = "test_global.lua";
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 = "test_local.lua";
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);

View file

@ -0,0 +1,64 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/lua/inputactions.hpp>
#include <components/lua/scriptscontainer.hpp>
#include <components/testing/util.hpp>
namespace
{
using namespace testing;
using namespace TestingOpenMW;
TEST(LuaInputActionsTest, MultiTree)
{
{
LuaUtil::InputAction::MultiTree tree;
auto a = tree.insert();
auto b = tree.insert();
auto c = tree.insert();
auto d = tree.insert();
EXPECT_TRUE(tree.multiEdge(c, { a, b }));
EXPECT_TRUE(tree.multiEdge(a, { d }));
EXPECT_FALSE(tree.multiEdge(d, { c }));
}
{
LuaUtil::InputAction::MultiTree tree;
auto a = tree.insert();
auto b = tree.insert();
auto c = tree.insert();
EXPECT_TRUE(tree.multiEdge(b, { a }));
EXPECT_TRUE(tree.multiEdge(c, { a, b }));
}
}
TEST(LuaInputActionsTest, Registry)
{
sol::state lua;
LuaUtil::InputAction::Registry registry;
LuaUtil::InputAction::Info a({ "a", LuaUtil::InputAction::Type::Boolean, "test", "a_name", "a_description",
sol::make_object(lua, false) });
registry.insert(a);
LuaUtil::InputAction::Info b({ "b", LuaUtil::InputAction::Type::Boolean, "test", "b_name", "b_description",
sol::make_object(lua, false) });
registry.insert(b);
LuaUtil::Callback bindA({ lua.load("return function() return true end")(), sol::table(lua, sol::create) });
LuaUtil::Callback bindBToA(
{ lua.load("return function(_, _, aValue) return aValue end")(), sol::table(lua, sol::create) });
EXPECT_TRUE(registry.bind("a", bindA, {}));
EXPECT_TRUE(registry.bind("b", bindBToA, { "a" }));
registry.update(1.0);
sol::object bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean);
EXPECT_TRUE(bValue.is<bool>());
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<bool>());
bValue = registry.valueOfType("b", LuaUtil::InputAction::Type::Boolean);
EXPECT_TRUE(bValue.is<bool>() && bValue.as<bool>() == aValue.as<bool>());
}
}

View file

@ -0,0 +1,174 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/files/fixedpath.hpp>
#include <components/l10n/manager.hpp>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
#include <components/testing/util.hpp>
namespace
{
using namespace testing;
using namespace TestingOpenMW;
template <typename T>
T get(sol::state_view& lua, const std::string& luaCode)
{
return lua.safe_script("return " + luaCode).get<T>();
}
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<VFS::Manager> 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<std::string>(l, "t1('good_morning')"), "Guten Morgen.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile.");
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(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<std::string>(l, "t1('good_morning')"), "Good morning.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "You have one arrow.");
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"male\"})"), "He is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete.");
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=1})"), "You came in 1st place.");
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=100})"), "You came in 100th place.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=1})"), "There is one thing.");
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=100})"), "There are one hundred things.");
EXPECT_EQ(get<std::string>(l, "t1('duration', {num=100})"), "It took 1:40");
EXPECT_EQ(get<std::string>(l, "t1('numbers', {int=123, double=123.456})"),
"123 and 123 are integers, but 123.456 is a double");
EXPECT_EQ(get<std::string>(l, "t1('rounding', {value=123.456789})"), "123.46");
// Check that failed messages display the key instead of an empty string
EXPECT_EQ(get<std::string>(l, "t1('{mismatched_braces')"), "{mismatched_braces");
EXPECT_EQ(get<std::string>(l, "t1('{unknown_arg}')"), "{unknown_arg}");
EXPECT_EQ(get<std::string>(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<std::string>(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<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
EXPECT_EQ(get<std::string>(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<std::string>(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<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
});
}
}

View file

@ -1,14 +1,16 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/lua/luastate.hpp>
#include "../testing_util.hpp"
#include <components/testing/expecterror.hpp>
#include <components/testing/util.hpp>
namespace
{
using namespace testing;
constexpr VFS::Path::NormalizedView counterPath("aaa/counter.lua");
TestingOpenMW::VFSTestFile counterFile(R"X(
x = 42
return {
@ -17,8 +19,12 @@ return {
}
)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
@ -51,6 +57,11 @@ return {
}
)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;
@ -63,14 +74,24 @@ return {
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<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "aaa/counter.lua", &counterFile },
{ "bbb/tests.lua", &testsFile }, { "invalid.lua", &invalidScriptFile }, { "big.lua", &bigScriptFile },
{ "requireBig.lua", &requireBigScriptFile } });
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({
{ counterPath, &counterFile },
{ testsPath, &testsFile },
{ invalidPath, &invalidScriptFile },
{ bigPath, &bigScriptFile },
{ requireBigPath, &requireBigScriptFile },
{ metaIndexErrorPath, &metaIndexErrorFile },
});
LuaUtil::ScriptsConfiguration mCfg;
LuaUtil::LuaState mLua{ mVFS.get(), &mCfg };
@ -78,13 +99,14 @@ return {
TEST_F(LuaStateTest, Sandbox)
{
sol::table script1 = mLua.runInNewSandbox("aaa/counter.lua");
const VFS::Path::Normalized path(counterPath);
sol::table script1 = mLua.runInNewSandbox(path);
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 42);
LuaUtil::call(script1["inc"], 3);
EXPECT_EQ(LuaUtil::call(script1["get"]).get<int>(), 45);
sol::table script2 = mLua.runInNewSandbox("aaa/counter.lua");
sol::table script2 = mLua.runInNewSandbox(path);
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 42);
LuaUtil::call(script2["inc"], 1);
EXPECT_EQ(LuaUtil::call(script2["get"]).get<int>(), 43);
@ -94,37 +116,39 @@ return {
TEST_F(LuaStateTest, ToString)
{
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), 3.14)), "3.14");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), true)), "true");
EXPECT_EQ(LuaUtil::toString(sol::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.sol(), "something")), "\"something\"");
EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.unsafeState(), "something")), "\"something\"");
}
TEST_F(LuaStateTest, Cast)
{
EXPECT_EQ(LuaUtil::cast<int>(sol::make_object(mLua.sol(), 3.14)), 3);
EXPECT_ERROR(
LuaUtil::cast<int>(sol::make_object(mLua.sol(), "3.14")), "Value \"\"3.14\"\" can not be casted to int");
EXPECT_ERROR(LuaUtil::cast<std::string_view>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_EQ(LuaUtil::cast<int>(sol::make_object(mLua.unsafeState(), 3.14)), 3);
EXPECT_ERROR(LuaUtil::cast<int>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to int");
EXPECT_ERROR(LuaUtil::cast<std::string_view>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to string");
EXPECT_ERROR(LuaUtil::cast<std::string>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_ERROR(LuaUtil::cast<std::string>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to string");
EXPECT_ERROR(LuaUtil::cast<sol::table>(sol::make_object(mLua.sol(), sol::nil)),
EXPECT_ERROR(LuaUtil::cast<sol::table>(sol::make_object(mLua.unsafeState(), sol::nil)),
"Value \"nil\" can not be casted to sol::table");
EXPECT_ERROR(LuaUtil::cast<sol::function>(sol::make_object(mLua.sol(), "3.14")),
EXPECT_ERROR(LuaUtil::cast<sol::function>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to sol::function");
EXPECT_ERROR(LuaUtil::cast<sol::protected_function>(sol::make_object(mLua.sol(), "3.14")),
EXPECT_ERROR(LuaUtil::cast<sol::protected_function>(sol::make_object(mLua.unsafeState(), "3.14")),
"Value \"\"3.14\"\" can not be casted to sol::function");
}
TEST_F(LuaStateTest, ErrorHandling)
{
EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:");
const VFS::Path::Normalized path("invalid.lua");
EXPECT_ERROR(mLua.runInNewSandbox(path), "[string \"invalid.lua\"]:1:");
}
TEST_F(LuaStateTest, CustomRequire)
{
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
const VFS::Path::Normalized path("bbb/tests.lua");
sol::table script = mLua.runInNewSandbox(path);
EXPECT_FLOAT_EQ(
LuaUtil::call(script["sin"], 1).get<float>(), -LuaUtil::call(script["requireMathSin"], -1).get<float>());
@ -132,7 +156,7 @@ return {
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 43);
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 44);
{
sol::table script2 = mLua.runInNewSandbox("bbb/tests.lua");
sol::table script2 = mLua.runInNewSandbox(path);
EXPECT_EQ(LuaUtil::call(script2["useCounter"]).get<int>(), 43);
}
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 45);
@ -142,7 +166,8 @@ return {
TEST_F(LuaStateTest, ReadOnly)
{
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
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<int>(), 3);
@ -158,46 +183,51 @@ return {
TEST_F(LuaStateTest, Print)
{
const VFS::Path::Normalized path("bbb/tests.lua");
{
sol::table script = mLua.runInNewSandbox("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, "[bbb/tests.lua]:\t1\t2\t3\n");
EXPECT_EQ(output, "unnamed:\t1\t2\t3\n");
}
{
sol::table script = mLua.runInNewSandbox("bbb/tests.lua", "prefix");
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[bbb/tests.lua]:\n");
EXPECT_EQ(output, "prefix:\n");
}
}
TEST_F(LuaStateTest, UnsafeFunction)
{
sol::table script = mLua.runInNewSandbox("bbb/tests.lua");
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"));
sol::table api1 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api1"));
sol::table api2 = LuaUtil::makeReadOnly(lua.sol().create_table_with("name", "api2"));
const VFS::Path::Normalized path("bbb/tests.lua");
sol::table script1 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api1 } });
sol::table script1 = lua.runInNewSandbox(path, "", { { "test.api", api1 } });
lua.addCommonPackage("sqrlib", lua.sol().create_table_with("sqr", [](int x) { return x * x; }));
lua.addCommonPackage("sqrlib", view.sol().create_table_with("sqr", [](int x) { return x * x; }));
sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", { { "test.api", api2 } });
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<int>(), 9);
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");
EXPECT_EQ(LuaUtil::call(script2["apiName"]).get<std::string>(), "api2");
});
}
TEST_F(LuaStateTest, GetLuaVersion)
@ -209,18 +239,27 @@ return {
{
auto getMem = [&] {
for (int i = 0; i < 5; ++i)
lua_gc(mLua.sol(), LUA_GCCOLLECT, 0);
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("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("requireBig.lua");
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");
}
}

View file

@ -1,4 +1,4 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/esm/luascripts.hpp>
@ -6,19 +6,31 @@
#include <components/lua/asyncpackage.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#include <components/lua/scripttracker.hpp>
#include "../testing_util.hpp"
#include <components/testing/util.hpp>
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 = {
@ -33,6 +45,8 @@ return {
}
)X");
constexpr VFS::Path::NormalizedView stopEventPath("stopevent.lua");
VFSTestFile stopEventScript(R"X(
return {
eventHandlers = {
@ -44,6 +58,9 @@ return {
}
)X");
constexpr VFS::Path::NormalizedView loadSave1Path("loadsave1.lua");
constexpr VFS::Path::NormalizedView loadSave2Path("loadsave2.lua");
VFSTestFile loadSaveScript(R"X(
x = 0
y = 0
@ -70,6 +87,8 @@ return {
}
)X");
constexpr VFS::Path::NormalizedView testInterfacePath("testinterface.lua");
VFSTestFile interfaceScript(R"X(
return {
interfaceName = "TestInterface",
@ -80,6 +99,8 @@ return {
}
)X");
constexpr VFS::Path::NormalizedView overrideInterfacePath("overrideinterface.lua");
VFSTestFile overrideInterfaceScript(R"X(
local old = nil
local interface = {
@ -104,6 +125,8 @@ return {
}
)X");
constexpr VFS::Path::NormalizedView useInterfacePath("useinterface.lua");
VFSTestFile useInterfaceScript(R"X(
local interfaces = require('openmw.interfaces')
return {
@ -113,22 +136,72 @@ return {
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<VFS::Manager> mVFS = createTestVFS({
{ "invalid.lua", &invalidScript },
{ "incorrect.lua", &incorrectScript },
{ "empty.lua", &emptyScript },
{ "test1.lua", &testScript },
{ "test2.lua", &testScript },
{ "stopEvent.lua", &stopEventScript },
{ "loadSave1.lua", &loadSaveScript },
{ "loadSave2.lua", &loadSaveScript },
{ "testInterface.lua", &interfaceScript },
{ "overrideInterface.lua", &overrideInterfaceScript },
{ "useInterface.lua", &useInterfaceScript },
{ 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;
@ -149,42 +222,54 @@ 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<int> 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, VerifyStructure)
TEST_F(LuaScriptsContainerTest, addCustomScriptShouldNotStartInvalidScript)
{
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
{
testing::internal::CaptureStdout();
EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("invalid.lua")));
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(*mCfg.findId("incorrect.lua")));
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)
{
testing::internal::CaptureStdout();
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("empty.lua")));
EXPECT_FALSE(scripts.addCustomScript(*mCfg.findId("empty.lua"))); // already present
EXPECT_EQ(internal::GetCapturedStdout(), "");
}
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(*mCfg.findId("test1.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
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"
@ -194,12 +279,14 @@ CUSTOM, PLAYER: useInterface.lua
TEST_F(LuaScriptsContainerTest, CallEvent)
{
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
std::string X0 = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5));
std::string X1 = LuaUtil::serialize(mLua.sol().create_table_with("x", 1.5));
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();
@ -211,7 +298,7 @@ CUSTOM, PLAYER: useInterface.lua
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[stopevent.lua]:\t event1 1.5\n"
"Test[test1.lua]:\t event1 1.5\n");
}
{
@ -226,7 +313,7 @@ CUSTOM, PLAYER: useInterface.lua
scripts.receiveEvent("Event1", X0);
EXPECT_EQ(internal::GetCapturedStdout(),
"Test[test2.lua]:\t event1 0.5\n"
"Test[stopEvent.lua]:\t event1 0.5\n");
"Test[stopevent.lua]:\t event1 0.5\n");
}
{
testing::internal::CaptureStdout();
@ -240,10 +327,13 @@ CUSTOM, PLAYER: useInterface.lua
TEST_F(LuaScriptsContainerTest, RemoveScript)
{
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test1.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("stopEvent.lua")));
EXPECT_TRUE(scripts.addCustomScript(*mCfg.findId("test2.lua")));
std::string X = LuaUtil::serialize(mLua.sol().create_table_with("x", 0.5));
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();
@ -253,11 +343,11 @@ CUSTOM, PLAYER: useInterface.lua
"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");
"Test[stopevent.lua]:\t event1 0.5\n");
}
{
testing::internal::CaptureStdout();
int stopEventScriptId = *mCfg.findId("stopEvent.lua");
const int stopEventScriptId = getId(stopEventPath);
EXPECT_TRUE(scripts.hasScript(stopEventScriptId));
scripts.removeScript(stopEventScriptId);
EXPECT_FALSE(scripts.hasScript(stopEventScriptId));
@ -271,7 +361,7 @@ CUSTOM, PLAYER: useInterface.lua
}
{
testing::internal::CaptureStdout();
scripts.removeScript(*mCfg.findId("test1.lua"));
scripts.removeScript(getId(test1Path));
scripts.update(1.5f);
scripts.receiveEvent("Event1", X);
EXPECT_EQ(internal::GetCapturedStdout(),
@ -288,19 +378,19 @@ CUSTOM, PLAYER: useInterface.lua
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[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()));
int addIfaceId = *mCfg.findId("testInterface.lua");
int overrideIfaceId = *mCfg.findId("overrideInterface.lua");
int useIfaceId = *mCfg.findId("useInterface.lua");
const int addIfaceId = getId(testInterfacePath);
const int overrideIfaceId = getId(overrideInterfacePath);
const int useIfaceId = getId(useInterfacePath);
testing::internal::CaptureStdout();
scripts.addAutoStartedScripts();
@ -315,11 +405,11 @@ CUSTOM, PLAYER: useInterface.lua
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[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)
@ -332,10 +422,11 @@ CUSTOM, PLAYER: useInterface.lua
scripts3.setAutoStartConf(mCfg.getPlayerConf());
scripts1.addAutoStartedScripts();
EXPECT_TRUE(scripts1.addCustomScript(*mCfg.findId("test1.lua")));
EXPECT_TRUE(scripts1.addCustomScript(getId(test1Path)));
scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with("n", 1, "x", 0.5, "y", 3.5)));
scripts1.receiveEvent("Set", LuaUtil::serialize(mLua.sol().create_table_with("n", 2, "x", 2.5, "y", 1.5)));
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);
@ -346,23 +437,23 @@ CUSTOM, PLAYER: useInterface.lua
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[loadsave2.lua]:\t0.5\t3.5\n"
"Test[loadsave1.lua]:\t2.5\t1.5\n"
"Test[test1.lua]:\tprint\n");
EXPECT_FALSE(scripts2.hasScript(*mCfg.findId("testInterface.lua")));
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"
"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[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(*mCfg.findId("testInterface.lua")));
EXPECT_TRUE(scripts3.hasScript(getId(testInterfacePath)));
}
}
@ -370,8 +461,8 @@ CUSTOM, PLAYER: useInterface.lua
{
using TimerType = LuaUtil::ScriptsContainer::TimerType;
LuaUtil::ScriptsContainer scripts(&mLua, "Test");
int test1Id = *mCfg.findId("test1.lua");
int test2Id = *mCfg.findId("test2.lua");
const int test1Id = getId(test1Path);
const int test2Id = getId(test2Path);
testing::internal::CaptureStdout();
EXPECT_TRUE(scripts.addCustomScript(test1Id));
@ -379,10 +470,10 @@ CUSTOM, PLAYER: useInterface.lua
EXPECT_EQ(internal::GetCapturedStdout(), "");
int counter1 = 0, counter2 = 0, counter3 = 0, counter4 = 0;
sol::function fn1 = sol::make_object(mLua.sol(), [&]() { counter1++; });
sol::function fn2 = sol::make_object(mLua.sol(), [&]() { counter2++; });
sol::function fn3 = sol::make_object(mLua.sol(), [&](int d) { counter3 += d; });
sol::function fn4 = sol::make_object(mLua.sol(), [&](int d) { counter4 += d; });
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);
@ -391,12 +482,16 @@ CUSTOM, PLAYER: useInterface.lua
scripts.processTimers(1, 2);
scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.sol(), 3));
scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.sol(), 4));
scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.sol(), 1));
scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.sol(), 2));
scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.sol(), 10));
scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.sol(), 20));
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);
@ -446,7 +541,8 @@ CUSTOM, PLAYER: useInterface.lua
TEST_F(LuaScriptsContainerTest, CallbackWrapper)
{
LuaUtil::Callback callback{ mLua.sol()["print"], mLua.newTable() };
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 };
@ -458,10 +554,99 @@ CUSTOM, PLAYER: useInterface.lua
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>());
sol::table table = deserialized;
for (const auto& [key, value] : table)
{
EXPECT_TRUE(key.is<ESM::RefNum>());
EXPECT_TRUE(value.is<ESM::RefNum>());
EXPECT_EQ(key.as<ESM::RefNum>(), (ESM::RefNum{ 42, 34 }));
EXPECT_EQ(value.as<ESM::RefNum>(), (ESM::RefNum{ 13, 12 }));
return;
}
EXPECT_FALSE(true);
});
}
}

View file

@ -1,4 +1,4 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <osg/Matrixf>
@ -13,7 +13,7 @@
#include <components/misc/color.hpp>
#include <components/misc/endianness.hpp>
#include "../testing_util.hpp"
#include <components/testing/expecterror.hpp>
namespace
{

View file

@ -0,0 +1,128 @@
#include <filesystem>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/lua/asyncpackage.hpp>
#include <components/lua/storage.hpp>
namespace
{
using namespace testing;
template <typename T>
T get(sol::state_view& lua, std::string luaCode)
{
return lua.safe_script("return " + luaCode).get<T>();
}
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<int>(lua, "mutable:get('x')"), 5);
EXPECT_EQ(get<int>(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<int>(lua, "t1.x"), 5);
EXPECT_EQ(get<int>(lua, "t2.x"), 5);
lua.safe_script("mutable:reset()");
EXPECT_TRUE(get<bool>(lua, "ro:get('x') == nil"));
lua.safe_script("mutable:reset({x=4, y=7})");
EXPECT_EQ(get<int>(lua, "ro:get('x')"), 4);
EXPECT_EQ(get<int>(lua, "ro:get('y')"), 7);
EXPECT_THAT(get<std::string>(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<int>(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<int>(lua, "mutable:get('x').z"), 7);
EXPECT_EQ(get<int>(lua, "ro:get('x').z"), 7);
EXPECT_EQ(get<std::string>(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<int>(lua, "permanent:get('x')"), 1);
EXPECT_EQ(get<int>(lua, "temporary:get('y')"), 2);
storage.clearTemporaryAndRemoveCallbacks();
lua["permanent"] = storage.getMutableSection(lua, "permanent");
lua["temporary"] = storage.getMutableSection(lua, "temporary");
EXPECT_EQ(get<int>(lua, "permanent:get('x')"), 1);
EXPECT_TRUE(get<bool>(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<int>(lua, "permanent:get('x')"), 1);
EXPECT_TRUE(get<bool>(lua, "permanent:get('z') == nil"));
EXPECT_TRUE(get<bool>(lua, "temporary:get('y') == nil"));
});
}
}

View file

@ -14,7 +14,8 @@ namespace
sol::protected_function mNew;
LuaUiContentTest()
{
mLuaState.addInternalLibSearchPath("resources/lua_libs");
mLuaState.addInternalLibSearchPath(
std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "components" / "lua_ui");
mNew = LuaUi::loadContentConstructor(&mLuaState);
}
@ -26,7 +27,7 @@ namespace
return LuaUi::ContentView(result.get<sol::table>());
}
sol::table makeTable() { return sol::table(mLuaState.sol(), sol::create); }
sol::table makeTable() { return sol::table(mLuaState.unsafeState(), sol::create); }
sol::table makeTable(std::string name)
{
@ -38,13 +39,14 @@ namespace
TEST_F(LuaUiContentTest, ProtectedMetatable)
{
mLuaState.sol()["makeContent"] = mNew;
mLuaState.sol()["M"] = makeContent(makeTable()).getMetatable();
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(mLuaState.sol().safe_script(testScript));
EXPECT_NO_THROW(sol.safe_script(testScript));
}
TEST_F(LuaUiContentTest, Create)

View file

@ -1,10 +1,9 @@
#include "gmock/gmock.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <components/lua/luastate.hpp>
#include <components/lua/utilpackage.hpp>
#include "../testing_util.hpp"
#include <components/testing/expecterror.hpp>
namespace
{
@ -50,6 +49,10 @@ namespace
EXPECT_TRUE(get<bool>(lua, "ediv0.x == math.huge and ediv0.y ~= ediv0.y"));
EXPECT_TRUE(get<bool>(lua, "util.vector2(1, 2):emul(util.vector2(3, 4)) == util.vector2(3, 8)"));
EXPECT_TRUE(get<bool>(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<bool>(lua, "swizzle.xx == util.vector2(1, 1) and swizzle.yy == util.vector2(2, 2)"));
EXPECT_TRUE(get<bool>(lua, "swizzle.y0 == util.vector2(2, 0) and swizzle.x1 == util.vector2(1, 1)"));
EXPECT_TRUE(get<bool>(lua, "swizzle['01'] == util.vector2(0, 1) and swizzle['0y'] == util.vector2(0, 2)"));
}
TEST(LuaUtilPackageTest, Vector3)
@ -83,6 +86,12 @@ namespace
EXPECT_TRUE(get<bool>(lua, "ediv0.z == math.huge"));
EXPECT_TRUE(get<bool>(lua, "util.vector3(1, 2, 3):emul(util.vector3(3, 4, 5)) == util.vector3(3, 8, 15)"));
EXPECT_TRUE(get<bool>(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<bool>(lua, "swizzle.xxx == util.vector3(1, 1, 1)"));
EXPECT_TRUE(get<bool>(lua, "swizzle.xyz == swizzle.zyx.zyx"));
EXPECT_TRUE(get<bool>(lua, "swizzle.xy0 == util.vector3(1, 2, 0) and swizzle.x11 == util.vector3(1, 1, 1)"));
EXPECT_TRUE(
get<bool>(lua, "swizzle['001'] == util.vector3(0, 0, 1) and swizzle['0yx'] == util.vector3(0, 2, 1)"));
}
TEST(LuaUtilPackageTest, Vector4)
@ -117,6 +126,14 @@ namespace
get<bool>(lua, "util.vector4(1, 2, 3, 4):emul(util.vector4(3, 4, 5, 6)) == util.vector4(3, 8, 15, 24)"));
EXPECT_TRUE(
get<bool>(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<bool>(lua, "swizzle.wwww == util.vector4(4, 4, 4, 4)"));
EXPECT_TRUE(get<bool>(lua, "swizzle.xyzw == util.vector4(1, 2, 3, 4)"));
EXPECT_TRUE(get<bool>(lua, "swizzle.xyzw == swizzle.wzyx.wzyx"));
EXPECT_TRUE(
get<bool>(lua, "swizzle.xyz0 == util.vector4(1, 2, 3, 0) and swizzle.w110 == util.vector4(4, 1, 1, 0)"));
EXPECT_TRUE(get<bool>(
lua, "swizzle['0001'] == util.vector4(0, 0, 0, 1) and swizzle['0yx1'] == util.vector4(0, 2, 1, 1)"));
}
TEST(LuaUtilPackageTest, Color)

View file

@ -0,0 +1,357 @@
#include <gtest/gtest.h>
#include <sol/object.hpp>
#include <sol/state.hpp>
#include <sol/table.hpp>
#include <yaml-cpp/yaml.h>
#include <components/lua/yamlloader.hpp>
namespace
{
template <typename T>
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<T>() == 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<bool>() == 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<double>());
}
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<std::string>() == 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<std::string>() == 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<double>::max()));
ASSERT_TRUE(checkNumber(lua, "-1.7976931348623157E+308", std::numeric_limits<double>::lowest()));
ASSERT_TRUE(checkNumber(lua, "2.2250738585072014e-308", std::numeric_limits<double>::min()));
ASSERT_TRUE(checkNumber(lua, ".inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.inf", -std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, ".Inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.Inf", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.Inf", -std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, ".INF", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "+.INF", std::numeric_limits<double>::infinity()));
ASSERT_TRUE(checkNumber(lua, "-.INF", -std::numeric_limits<double>::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<sol::table>()["x"], sol::nil);
ASSERT_EQ(map.as<sol::table>()["y"], 2);
ASSERT_EQ(map.as<sol::table>()[4], 5);
sol::object array = LuaUtil::loadYaml("[ 3, 4 ]", lua);
ASSERT_EQ(array.as<sol::table>()[1], 3);
sol::object emptyTable = LuaUtil::loadYaml("{}", lua);
ASSERT_TRUE(emptyTable.as<sol::table>().empty());
sol::object emptyArray = LuaUtil::loadYaml("[]", lua);
ASSERT_TRUE(emptyArray.as<sol::table>().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<sol::table>()[1], std::string("First Scalar"));
ASSERT_EQ(scalarArray.as<sol::table>()[2], 1);
ASSERT_EQ(scalarArray.as<sol::table>()[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<sol::table>()["string"], std::string("str"));
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["integer"], 65);
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["float"], 0.278);
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["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<sol::table>()["x"][3], true);
ASSERT_EQ(mapOfArrays.as<sol::table>()["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<sol::table>()[1]["avg"], 0.278);
ASSERT_EQ(arrayOfMaps.as<sol::table>()[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<sol::table>()[1][2], 65);
ASSERT_EQ(arrayOfArrays.as<sol::table>()[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<sol::table>()["Name1"]["hr"], 65);
ASSERT_EQ(mapOfMaps.as<sol::table>()["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<sol::table>()[1][1], std::string("First Scalar"));
ASSERT_EQ(twoDocuments.as<sol::table>()[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<sol::table>()["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<sol::table>()[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<sol::table>()["plain"] == std::string("This unquoted scalar spans many lines."));
ASSERT_TRUE(multiLinePlanarScalars.as<sol::table>()["quoted"] == std::string("So does this quoted scalar.\n"));
}
}

View file

@ -1,20 +1,16 @@
#include <gtest/gtest.h>
#include <components/debug/debugging.hpp>
#include <components/misc/strings/conversion.hpp>
#include <components/settings/parser.hpp>
#include <components/settings/values.hpp>
#include "components/misc/strings/conversion.hpp"
#include "components/settings/parser.hpp"
#include "components/settings/values.hpp"
#include <gtest/gtest.h>
#include <filesystem>
#ifdef WIN32
// we cannot use GTEST_API_ before main if we're building standalone exe application,
// and we're linking GoogleTest / GoogleMock as DLLs and not linking gtest_main / gmock_main
int main(int argc, char** argv)
{
#else
GTEST_API_ int main(int argc, char** argv)
{
#endif
Log::sMinDebugLevel = Debug::getDebugLevel();
const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files"
/ Misc::StringUtils::stringToU8String("settings-default.cfg");

View file

@ -1,5 +1,6 @@
#include "../testing_util.hpp"
#include "components/misc/resourcehelpers.hpp"
#include <components/misc/resourcehelpers.hpp>
#include <components/testing/util.hpp>
#include <gtest/gtest.h>
namespace
@ -7,27 +8,24 @@ namespace
using namespace Misc::ResourceHelpers;
TEST(CorrectSoundPath, wav_files_not_overridden_with_mp3_in_vfs_are_not_corrected)
{
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/bar.wav", nullptr } });
EXPECT_EQ(correctSoundPath("sound/bar.wav", mVFS.get()), "sound/bar.wav");
constexpr VFS::Path::NormalizedView path("sound/bar.wav");
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { path, nullptr } });
EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/bar.wav");
}
TEST(CorrectSoundPath, wav_files_overridden_with_mp3_in_vfs_are_corrected)
{
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({ { "sound/foo.mp3", nullptr } });
EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3");
constexpr VFS::Path::NormalizedView mp3("sound/foo.mp3");
std::unique_ptr<VFS::Manager> 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<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctSoundPath("sound/foo.wav", mVFS.get()), "sound/foo.mp3");
}
TEST(CorrectSoundPath, correct_path_normalize_paths)
{
std::unique_ptr<VFS::Manager> mVFS = TestingOpenMW::createTestVFS({});
EXPECT_EQ(correctSoundPath("sound\\foo.wav", mVFS.get()), "sound/foo.mp3");
EXPECT_EQ(correctSoundPath("SOUND\\foo.WAV", mVFS.get()), "sound/foo.mp3");
constexpr VFS::Path::NormalizedView path("sound/foo.wav");
EXPECT_EQ(correctSoundPath(path, *mVFS), "sound/foo.mp3");
}
namespace

View file

@ -0,0 +1,194 @@
#include <components/misc/mathutil.hpp>
#include <osg/io_utils>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <iomanip>
#include <limits>
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<float>::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<float>::max_exponent10) << "osg::Quat(" << value.x()
<< ", " << value.y() << ", " << value.z() << ", " << value.w() << ')';
}
}
namespace Misc
{
namespace
{
using namespace testing;
struct MiscToEulerAnglesXZQuatTest : TestWithParam<std::pair<osg::Quat, osg::Vec3f>>
{
};
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<osg::Quat, osg::Vec3f> eulerAnglesXZQuat[] = {
{
osg::Quat(1, 0, 0, 0),
osg::Vec3f(0, 0, osg::PI),
},
{
osg::Quat(0, 1, 0, 0),
osg::Vec3f(0, 0, 0),
},
{
osg::Quat(0, 0, 1, 0),
osg::Vec3f(0, 0, osg::PI),
},
{
osg::Quat(0, 0, 0, 1),
osg::Vec3f(0, 0, 0),
},
{
osg::Quat(-0.5, -0.5, -0.5, -0.5),
osg::Vec3f(-osg::PI_2f, 0, 0),
},
{
osg::Quat(0.5, -0.5, -0.5, -0.5),
osg::Vec3f(0, 0, -osg::PI_2f),
},
{
osg::Quat(0.5, 0.5, -0.5, -0.5),
osg::Vec3f(osg::PI_2f, 0, 0),
},
{
osg::Quat(0.5, 0.5, 0.5, -0.5),
osg::Vec3f(0, 0, osg::PI_2f),
},
{
osg::Quat(0.5, 0.5, 0.5, 0.5),
osg::Vec3f(-osg::PI_2f, 0, 0),
},
{
// normalized osg::Quat(0.1, 0.2, 0.3, 0.4)
osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(-0.72972762584686279296875f, 0, -1.10714876651763916015625f),
},
{
osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(-0.13373161852359771728515625f, 0, -1.2277724742889404296875f),
},
{
osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(0.13373161852359771728515625f, 0, -1.2277724742889404296875f),
},
{
osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143),
osg::Vec3f(0.13373161852359771728515625f, 0, 1.2277724742889404296875f),
},
{
osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143),
osg::Vec3f(-0.13373161852359771728515625, 0, 1.2277724742889404296875f),
},
{
osg::Quat(0.246736, -0.662657, -0.662667, 0.246739),
osg::Vec3f(-osg::PI_2f, 0, 2.5199801921844482421875f),
},
};
INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesXZQuatTest, ValuesIn(eulerAnglesXZQuat));
struct MiscToEulerAnglesZYXQuatTest : TestWithParam<std::pair<osg::Quat, osg::Vec3f>>
{
};
TEST_P(MiscToEulerAnglesZYXQuatTest, shouldReturnValueCloseTo)
{
const osg::Vec3f result = toEulerAnglesZYX(GetParam().first);
EXPECT_THAT(result, Vec3fEq(GetParam().second, std::numeric_limits<float>::epsilon()))
<< "toEulerAnglesZYX(" << GetParam().first << ") = " << result;
}
const std::pair<osg::Quat, osg::Vec3f> eulerAnglesZYXQuat[] = {
{
osg::Quat(1, 0, 0, 0),
osg::Vec3f(osg::PI, 0, 0),
},
{
osg::Quat(0, 1, 0, 0),
osg::Vec3f(osg::PI, 0, osg::PI),
},
{
osg::Quat(0, 0, 1, 0),
osg::Vec3f(0, 0, osg::PI),
},
{
osg::Quat(0, 0, 0, 1),
osg::Vec3f(0, 0, 0),
},
{
osg::Quat(-0.5, -0.5, -0.5, -0.5),
osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f),
},
{
osg::Quat(0.5, -0.5, -0.5, -0.5),
osg::Vec3f(osg::PI_2f, 0, -osg::PI_2f),
},
{
osg::Quat(0.5, 0.5, -0.5, -0.5),
osg::Vec3f(0, osg::PI_2f, -osg::PI_2f),
},
{
osg::Quat(0.5, 0.5, 0.5, -0.5),
osg::Vec3f(osg::PI_2f, 0, osg::PI_2f),
},
{
osg::Quat(0.5, 0.5, 0.5, 0.5),
osg::Vec3f(0, -osg::PI_2f, -osg::PI_2f),
},
{
// normalized osg::Quat(0.1, 0.2, 0.3, 0.4)
osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(0.1973955929279327392578125f, -0.8232119083404541015625f, -1.37340080738067626953125f),
},
{
osg::Quat(-0.18257418583505536, 0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(0.78539812564849853515625f, -0.339836895465850830078125f, -1.428899288177490234375f),
},
{
osg::Quat(0.18257418583505536, -0.36514837167011072, 0.54772255750516607, 0.73029674334022143),
osg::Vec3f(-0.78539812564849853515625f, 0.339836895465850830078125f, -1.428899288177490234375f),
},
{
osg::Quat(0.18257418583505536, 0.36514837167011072, -0.54772255750516607, 0.73029674334022143),
osg::Vec3f(-0.78539812564849853515625f, -0.339836895465850830078125f, 1.428899288177490234375f),
},
{
osg::Quat(0.18257418583505536, 0.36514837167011072, 0.54772255750516607, -0.73029674334022143),
osg::Vec3f(0.78539812564849853515625f, 0.339836895465850830078125f, 1.428899288177490234375f),
},
{
osg::Quat(0.246736, -0.662657, 0.246739, -0.662667),
osg::Vec3f(0.06586204469203948974609375f, -osg::PI_2f, 0.64701664447784423828125f),
},
};
INSTANTIATE_TEST_SUITE_P(FromQuat, MiscToEulerAnglesZYXQuatTest, ValuesIn(eulerAnglesZYXQuat));
}
}

View file

@ -0,0 +1,71 @@
#ifndef OPENMW_TEST_SUITE_NIF_NODE_H
#define OPENMW_TEST_SUITE_NIF_NODE_H
#include <components/nif/data.hpp>
#include <components/nif/node.hpp>
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<NiObjectNET&>(value));
value.mFlags = 0;
init(value.mTransform);
}
inline void init(NiGeometry& value)
{
init(static_cast<NiAVObject&>(value));
value.mData = NiGeometryDataPtr(nullptr);
value.mSkin = NiSkinInstancePtr(nullptr);
}
inline void init(NiTriShape& value)
{
init(static_cast<NiGeometry&>(value));
value.recType = RC_NiTriShape;
}
inline void init(NiTriStrips& value)
{
init(static_cast<NiGeometry&>(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

View file

@ -3,6 +3,7 @@
#include <components/nif/node.hpp>
#include <components/nif/property.hpp>
#include <components/nifosg/nifloader.hpp>
#include <components/resource/bgsmfilemanager.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/sceneutil/serialize.hpp>
#include <components/vfs/manager.hpp>
@ -25,10 +26,13 @@ namespace
using namespace NifOsg;
using namespace Nif::Testing;
constexpr VFS::Path::NormalizedView testNif("test.nif");
struct BaseNifOsgLoaderTest
{
VFS::Manager mVfs;
Resource::ImageManager mImageManager{ &mVfs };
Resource::ImageManager mImageManager{ &mVfs, 0 };
Resource::BgsmFileManager mMaterialManager{ &mVfs, 0 };
const osgDB::ReaderWriter* mReaderWriter = osgDB::Registry::instance()->getReaderWriterForExtension("osgt");
osg::ref_ptr<osgDB::Options> mOptions = new osgDB::Options;
@ -66,11 +70,11 @@ namespace
TEST_F(NifOsgLoaderTest, shouldLoadFileWithDefaultNode)
{
Nif::Node node;
Nif::NiAVObject node;
init(node);
Nif::NIFFile file("test.nif");
Nif::NIFFile file(testNif);
file.mRoots.push_back(&node);
auto result = Loader::load(file, &mImageManager);
auto result = Loader::load(file, &mImageManager, &mMaterialManager);
EXPECT_EQ(serialize(*result), R"(
osg::Group {
UniqueID 1
@ -108,7 +112,7 @@ osg::Group {
)");
}
std::string formatOsgNodeForShaderProperty(std::string_view shaderPrefix)
std::string formatOsgNodeForBSShaderProperty(std::string_view shaderPrefix)
{
std::ostringstream oss;
oss << R"(
@ -165,6 +169,73 @@ osg::Group {
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;
@ -183,18 +254,18 @@ osg::Group {
TEST_P(NifOsgLoaderBSShaderPrefixTest, shouldAddShaderPrefix)
{
Nif::Node node;
Nif::NiAVObject node;
init(node);
Nif::BSShaderPPLightingProperty property;
property.recType = Nif::RC_BSShaderPPLightingProperty;
property.textureSet = nullptr;
property.controller = nullptr;
property.type = GetParam().mShaderType;
node.props.push_back(Nif::RecordPtrT<Nif::Property>(&property));
Nif::NIFFile file("test.nif");
property.mTextureSet = nullptr;
property.mController = nullptr;
property.mType = GetParam().mShaderType;
node.mProperties.push_back(Nif::RecordPtrT<Nif::NiProperty>(&property));
Nif::NIFFile file(testNif);
file.mRoots.push_back(&node);
auto result = Loader::load(file, &mImageManager);
EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix));
auto result = Loader::load(file, &mImageManager, &mMaterialManager);
EXPECT_EQ(serialize(*result), formatOsgNodeForBSShaderProperty(GetParam().mExpectedShaderPrefix));
}
INSTANTIATE_TEST_SUITE_P(Params, NifOsgLoaderBSShaderPrefixTest, ValuesIn(NifOsgLoaderBSShaderPrefixTest::sParams));
@ -211,18 +282,20 @@ osg::Group {
TEST_P(NifOsgLoaderBSLightingShaderPrefixTest, shouldAddShaderPrefix)
{
Nif::Node node;
Nif::NiAVObject node;
init(node);
Nif::BSLightingShaderProperty property;
property.recType = Nif::RC_BSLightingShaderProperty;
property.mTextureSet = nullptr;
property.controller = nullptr;
property.type = GetParam().mShaderType;
node.props.push_back(Nif::RecordPtrT<Nif::Property>(&property));
Nif::NIFFile file("test.nif");
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<Nif::NiProperty>(&property));
Nif::NIFFile file(testNif);
file.mRoots.push_back(&node);
auto result = Loader::load(file, &mImageManager);
EXPECT_EQ(serialize(*result), formatOsgNodeForShaderProperty(GetParam().mExpectedShaderPrefix));
auto result = Loader::load(file, &mImageManager, &mMaterialManager);
EXPECT_EQ(serialize(*result), formatOsgNodeForBSLightingShaderProperty(GetParam().mExpectedShaderPrefix));
}
INSTANTIATE_TEST_SUITE_P(

View file

@ -0,0 +1,377 @@
#include <components/resource/objectcache.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <osg/Object>
namespace Resource
{
namespace
{
using namespace ::testing;
TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheShouldReturnNullptrByDefault)
{
osg::ref_ptr<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
EXPECT_EQ(cache->getRefFromObjectCache(42), nullptr);
}
TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldReturnNulloptByDefault)
{
osg::ref_ptr<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key = 42;
osg::ref_ptr<Object> value(new Object);
cache->addEntryToObjectCache(key, value);
EXPECT_EQ(cache->getRefFromObjectCache(key), value);
}
TEST(ResourceGenericObjectCacheTest, shouldStoreNullptrValues)
{
osg::ref_ptr<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key = 42;
cache->addEntryToObjectCache(key, nullptr);
EXPECT_THAT(cache->getRefFromObjectCacheOrNone(key), Optional(nullptr));
}
TEST(ResourceGenericObjectCacheTest, updateShouldExtendLifetimeForItemsWithZeroTimestamp)
{
osg::ref_ptr<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key = 42;
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key = 42;
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const double referenceTime = 1;
const double expiryDelay = 1;
const int key = 42;
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const double referenceTime = 1;
const double expiryDelay = 1;
const int key = 42;
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const double referenceTime = 1;
const double expiryDelay = 2;
const int key = 42;
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
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<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key = 42;
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
const int key1 = 42;
const int key2 = 13;
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> value2(new Object);
osg::ref_ptr<Object> value3(new Object);
cache->addEntryToObjectCache(1, value1);
cache->addEntryToObjectCache(2, value2);
cache->addEntryToObjectCache(3, value3);
std::vector<std::pair<int, osg::Object*>> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
{
const CacheStats stats = cache->getStats();
EXPECT_EQ(stats.mGet, 0);
EXPECT_EQ(stats.mHit, 0);
}
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> value2(new Object);
osg::ref_ptr<Object> 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<GenericObjectCache<int>> cache(new GenericObjectCache<int>);
osg::ref_ptr<Object> value1(new Object);
osg::ref_ptr<Object> value2(new Object);
osg::ref_ptr<Object> 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<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
const std::string key = "key";
osg::ref_ptr<Object> value(new Object);
cache->addEntryToObjectCache(std::string_view("key"), value);
EXPECT_EQ(cache->getRefFromObjectCache(key), value);
}
TEST(ResourceGenericObjectCacheTest, addEntryToObjectCacheShouldKeyMoving)
{
osg::ref_ptr<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
std::string key(128, 'a');
osg::ref_ptr<Object> 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<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
const std::string key = "key";
osg::ref_ptr<Object> 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<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
const std::string key = "key";
osg::ref_ptr<Object> value(new Object);
cache->addEntryToObjectCache(key, value);
EXPECT_EQ(cache->getRefFromObjectCache(std::string_view("key")), value);
}
TEST(ResourceGenericObjectCacheTest, getRefFromObjectCacheOrNoneShouldSupportHeterogeneousLookup)
{
osg::ref_ptr<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
const std::string key = "key";
osg::ref_ptr<Object> value(new Object);
cache->addEntryToObjectCache(key, value);
EXPECT_THAT(cache->getRefFromObjectCacheOrNone(std::string_view("key")), Optional(value));
}
TEST(ResourceGenericObjectCacheTest, checkInObjectCacheShouldSupportHeterogeneousLookup)
{
osg::ref_ptr<GenericObjectCache<std::string>> cache(new GenericObjectCache<std::string>);
const std::string key = "key";
osg::ref_ptr<Object> value(new Object);
cache->addEntryToObjectCache(key, value);
EXPECT_TRUE(cache->checkInObjectCache(std::string_view("key"), 0));
}
}
}

View file

@ -0,0 +1,131 @@
#include <components/sceneutil/osgacontroller.hpp>
#include <gtest/gtest.h>
#include <osgAnimation/Channel>
#include <filesystem>
#include <fstream>
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<Resource::Animation> createMergedAnimationTrack(std::string name, osg::Matrixf startTransform,
osg::Matrixf endTransform, float startTime = 0.0f, float endTime = 1.0f)
{
osg::ref_ptr<Resource::Animation> 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<osgAnimation::MatrixLinearChannel> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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<EmulatedAnimation> 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
}
}

View file

@ -1,11 +1,10 @@
#include <components/settings/parser.hpp>
#include <components/testing/util.hpp>
#include <fstream>
#include <gtest/gtest.h>
#include "../testing_util.hpp"
namespace
{
using namespace testing;

View file

@ -1,12 +1,11 @@
#include <components/settings/shadermanager.hpp>
#include <components/testing/util.hpp>
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include "../testing_util.hpp"
namespace
{
using namespace testing;

View file

@ -59,6 +59,33 @@ namespace Settings
EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1);
}
TEST_F(SettingsValuesTest, constructorWithDefaultShouldDoLookup)
{
Manager::mUserSettings[std::make_pair("category", "value")] = "13";
Index index;
SettingValue<int> 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<int> 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<int> value{ index, "category", "value", 42 };
EXPECT_EQ(value.get(), 42);
}
TEST_F(SettingsValuesTest, moveConstructorShouldSetDefaults)
{
Index index;
@ -79,6 +106,13 @@ namespace Settings
EXPECT_EQ(values.mCamera.mFieldOfView.get(), 1);
}
TEST_F(SettingsValuesTest, moveConstructorShouldThrowOnMissingSetting)
{
Index index;
SettingValue<int> defaultValue{ index, "category", "value", 42 };
EXPECT_THROW([&] { SettingValue<int> value(std::move(defaultValue)); }(), std::runtime_error);
}
TEST_F(SettingsValuesTest, findShouldThrowExceptionOnTypeMismatch)
{
Index index;

View file

@ -1,12 +1,11 @@
#include <components/files/conversion.hpp>
#include <components/shader/shadermanager.hpp>
#include <components/testing/util.hpp>
#include <fstream>
#include <gtest/gtest.h>
#include "../testing_util.hpp"
namespace
{
using namespace testing;

Some files were not shown because too many files have changed in this diff Show more