diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b89b99ac8..d171e8222 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,7 +60,7 @@ variables: &cs-targets - choco source add -n=openmw-proxy -s="https://repo.openmw.org/repository/Chocolately/" --priority=1 - choco install git --force --params "/GitAndUnixToolsOnPath" -y - choco install 7zip -y - - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' --version=3.18.0 -y + - choco install cmake.install --installargs 'ADD_CMAKE_TO_PATH=System' -y - choco install vswhere -y - choco install ninja -y - choco install python -y diff --git a/CHANGELOG.md b/CHANGELOG.md index c68e7b0f5..a00716f96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ Bug #1662: Qt4 and Windows binaries crash if there's a non-ASCII character in a file path/config path Bug #1952: Incorrect particle lighting + Bug #2069: Fireflies in Fireflies invade Morrowind look wrong Bug #2311: Targeted scripts are not properly supported on non-unique RefIDs Bug #3676: NiParticleColorModifier isn't applied properly Bug #3714: Savegame fails to load due to conflict between SpellState and MagicEffects Bug #4021: Attributes and skills are not stored as floats Bug #4055: Local scripts don't inherit variables from their base record Bug #4623: Corprus implementation is incorrect + Bug #4631: Setting MSAA level too high doesn't fall back to highest supported level Bug #4764: Data race in osg ParticleSystem Bug #4774: Guards are ignorant of an invisible player that tries to attack them Bug #5108: Savegame bloating due to inefficient fog textures format @@ -44,8 +46,12 @@ Bug #5531: Actors flee using current rotation by axis x Bug #5539: Window resize breaks when going from a lower resolution to full screen resolution Bug #5548: Certain exhausted topics can be highlighted again even though there's no new dialogue + Bug #5557: Diagonal movement is noticeably slower with analogue stick + Bug #5603: Setting constant effect cast style doesn't correct effects view + Bug #5611: Usable items with "0 Uses" should be used only once Feature #390: 3rd person look "over the shoulder" Feature #2386: Distant Statics in the form of Object Paging + Feature #4894: Consider actors as obstacles for pathfinding Feature #5297: Add a search function to the "Datafiles" tab of the OpenMW launcher Feature #5362: Show the soul gems' trapped soul in count dialog Feature #5445: Handle NiLines @@ -55,6 +61,8 @@ Feature #5524: Resume failed script execution after reload Feature #5525: Search fields tweaks (utf-8) Feature #5545: Option to allow stealing from an unconscious NPC during combat + Feature #5579: MCP SetAngle enhancement + Feature #5610: Actors movement should be smoother Task #5480: Drop Qt4 support Task #5520: Improve cell name autocompleter implementation diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 616caf3fa..e8d65372d 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -69,7 +69,7 @@ NMAKE="" NINJA="" PDBS="" PLATFORM="" -CONFIGURATION="" +CONFIGURATIONS=() TEST_FRAMEWORK="" GOOGLE_INSTALL_ROOT="" INSTALL_PREFIX="." @@ -129,7 +129,7 @@ while [ $# -gt 0 ]; do PDBS=true ;; c ) - CONFIGURATION=$1 + CONFIGURATIONS+=( $1 ) shift ;; t ) @@ -143,8 +143,10 @@ while [ $# -gt 0 ]; do cat < + -c Set the configuration, can also be set with environment variable CONFIGURATION. + For mutli-config generators, this is ignored, and all configurations are set up. + For single-config generators, several configurations can be set up at once by specifying -c multiple times. -d Skip checking the downloads. -D @@ -164,7 +166,7 @@ Options: -v <2017/2019> Choose the Visual Studio version to use. -n - Produce NMake makefiles instead of a Visual Studio solution. Cannout be used with -N. + Produce NMake makefiles instead of a Visual Studio solution. Cannot be used with -N. -N Produce Ninja (multi-config if CMake is new enough to support it) files instead of a Visual Studio solution. Cannot be used with -n.. -P @@ -187,7 +189,7 @@ done if [ -n "$NMAKE" ] || [ -n "$NINJA" ]; then if [ -n "$NMAKE" ] && [ -n "$NINJA" ]; then - echo "Cannout run in NMake and Ninja mode at the same time." + echo "Cannot run in NMake and Ninja mode at the same time." wrappedExit 1 fi ACTIVATE_MSVC=true @@ -293,29 +295,40 @@ add_cmake_opts() { CMAKE_OPTS="$CMAKE_OPTS $@" } -RUNTIME_DLLS="" +declare -A RUNTIME_DLLS +RUNTIME_DLLS["Release"]="" +RUNTIME_DLLS["Debug"]="" +RUNTIME_DLLS["RelWithDebInfo"]="" add_runtime_dlls() { - RUNTIME_DLLS="$RUNTIME_DLLS $@" + local CONFIG=$1 + shift + RUNTIME_DLLS[$CONFIG]="${RUNTIME_DLLS[$CONFIG]} $@" } -OSG_PLUGINS="" +declare -A OSG_PLUGINS +OSG_PLUGINS["Release"]="" +OSG_PLUGINS["Debug"]="" +OSG_PLUGINS["RelWithDebInfo"]="" add_osg_dlls() { - OSG_PLUGINS="$OSG_PLUGINS $@" + local CONFIG=$1 + shift + OSG_PLUGINS[$CONFIG]="${OSG_PLUGINS[$CONFIG]} $@" } -QT_PLATFORMS="" +declare -A QT_PLATFORMS +QT_PLATFORMS["Release"]="" +QT_PLATFORMS["Debug"]="" +QT_PLATFORMS["RelWithDebInfo"]="" add_qt_platform_dlls() { - QT_PLATFORMS="$QT_PLATFORMS $@" + local CONFIG=$1 + shift + QT_PLATFORMS[$CONFIG]="${QT_PLATFORMS[$CONFIG]} $@" } if [ -z $PLATFORM ]; then PLATFORM="$(uname -m)" fi -if [ -z $CONFIGURATION ]; then - CONFIGURATION="Debug" -fi - if [ -z $VS_VERSION ]; then VS_VERSION="2017" fi @@ -377,23 +390,6 @@ case $PLATFORM in ;; esac -case $CONFIGURATION in - debug|Debug|DEBUG ) - CONFIGURATION=Debug - BUILD_CONFIG=Debug - ;; - - release|Release|RELEASE ) - CONFIGURATION=Release - BUILD_CONFIG=Release - ;; - - relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) - CONFIGURATION=Release - BUILD_CONFIG=RelWithDebInfo - ;; -esac - if [ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ]; then GENERATOR="${GENERATOR} Win64" fi @@ -411,6 +407,79 @@ if [ -n "$NINJA" ]; then fi fi +if [ -n "$SINGLE_CONFIG" ]; then + if [ ${#CONFIGURATIONS[@]} -eq 0 ]; then + if [ -n "${CONFIGURATION:-}" ]; then + CONFIGURATIONS=("$CONFIGURATION") + else + CONFIGURATIONS=("Debug") + fi + elif [ ${#CONFIGURATIONS[@]} -ne 1 ]; then + # It's simplest just to recursively call the script a few times. + RECURSIVE_OPTIONS=() + if [ -n "$VERBOSE" ]; then + RECURSIVE_OPTIONS+=("-V") + fi + if [ -n "$SKIP_DOWNLOAD" ]; then + RECURSIVE_OPTIONS+=("-d") + fi + if [ -n "$BULLET_DOUBLE" ]; then + RECURSIVE_OPTIONS+=("-D") + fi + if [ -n "$SKIP_EXTRACT" ]; then + RECURSIVE_OPTIONS+=("-e") + fi + if [ -n "$KEEP" ]; then + RECURSIVE_OPTIONS+=("-k") + fi + if [ -n "$UNITY_BUILD" ]; then + RECURSIVE_OPTIONS+=("-u") + fi + if [ -n "$NMAKE" ]; then + RECURSIVE_OPTIONS+=("-n") + fi + if [ -n "$NINJA" ]; then + RECURSIVE_OPTIONS+=("-N") + fi + if [ -n "$PDBS" ]; then + RECURSIVE_OPTIONS+=("-P") + fi + if [ -n "$TEST_FRAMEWORK" ]; then + RECURSIVE_OPTIONS+=("-t") + fi + RECURSIVE_OPTIONS+=("-v $VS_VERSION") + RECURSIVE_OPTIONS+=("-p $PLATFORM") + RECURSIVE_OPTIONS+=("-i '$INSTALL_PREFIX'") + + for config in ${CONFIGURATIONS[@]}; do + $0 ${RECURSIVE_OPTIONS[@]} -c $config + done + + wrappedExit 1 + fi +else + if [ ${#CONFIGURATIONS[@]} -ne 0 ]; then + echo "Ignoring configurations argument - generator is multi-config" + fi + CONFIGURATIONS=("Release" "Debug" "RelWithDebInfo") +fi + +for i in ${!CONFIGURATIONS[@]}; do + case ${CONFIGURATIONS[$i]} in + debug|Debug|DEBUG ) + CONFIGURATIONS[$i]=Debug + ;; + + release|Release|RELEASE ) + CONFIGURATIONS[$i]=Release + ;; + + relwithdebinfo|RelWithDebInfo|RELWITHDEBINFO ) + CONFIGURATIONS[$i]=RelWithDebInfo + ;; + esac +done + if [ $MSVC_REAL_VER -ge 16 ] && [ -z "$NMAKE" ] && [ -z "$NINJA" ]; then if [ $BITS -eq 64 ]; then add_cmake_opts "-G\"$GENERATOR\" -A x64" @@ -422,7 +491,7 @@ else fi if [ -n "$SINGLE_CONFIG" ]; then - add_cmake_opts "-DCMAKE_BUILD_TYPE=${BUILD_CONFIG}" + add_cmake_opts "-DCMAKE_BUILD_TYPE=${CONFIGURATIONS[0]}" fi if ! [ -z $UNITY_BUILD ]; then @@ -454,52 +523,52 @@ if [ -z $SKIP_DOWNLOAD ]; then # Boost if [ -z $APPVEYOR ]; then download "Boost ${BOOST_VER}" \ - "https://sourceforge.net/projects/boost/files/boost-binaries/${BOOST_VER}/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/${BOOST_VER}/boost_${BOOST_VER_URL}-msvc-${MSVC_VER}-${BITS}.exe" \ "boost-${BOOST_VER}-msvc${MSVC_VER}-win${BITS}.exe" fi # Bullet download "Bullet 2.89 (${BULLET_DBL_DISPLAY})" \ - "https://rgw.ctrl-c.liu.se/openmw/Deps/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}${BULLET_DBL}.7z" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}${BULLET_DBL}.7z" \ "Bullet-2.89-msvc${MSVC_YEAR}-win${BITS}${BULLET_DBL}.7z" # FFmpeg download "FFmpeg 4.2.2" \ - "https://ffmpeg.zeranoe.com/builds/win${BITS}/shared/ffmpeg-4.2.2-win${BITS}-shared.zip" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-win${BITS}.zip" \ "ffmpeg-4.2.2-win${BITS}.zip" \ - "https://ffmpeg.zeranoe.com/builds/win${BITS}/dev/ffmpeg-4.2.2-win${BITS}-dev.zip" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/ffmpeg-4.2.2-dev-win${BITS}.zip" \ "ffmpeg-4.2.2-dev-win${BITS}.zip" # MyGUI download "MyGUI 3.4.0" \ - "https://rgw.ctrl-c.liu.se/openmw/Deps/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" if [ -n "$PDBS" ]; then download "MyGUI symbols" \ - "https://rgw.ctrl-c.liu.se/openmw/Deps/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" fi # OpenAL download "OpenAL-Soft 1.20.1" \ - "http://openal-soft.org/openal-binaries/openal-soft-1.20.1-bin.zip" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OpenAL-Soft-1.20.1.zip" \ "OpenAL-Soft-1.20.1.zip" # OSG download "OpenSceneGraph 3.6.5" \ - "https://rgw.ctrl-c.liu.se/openmw/Deps/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" \ "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}.7z" if [ -n "$PDBS" ]; then download "OpenSceneGraph symbols" \ - "https://rgw.ctrl-c.liu.se/openmw/Deps/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" \ "OSG-3.6.5-msvc${MSVC_REAL_YEAR}-win${BITS}-sym.7z" fi # SDL2 download "SDL 2.0.12" \ - "https://www.libsdl.org/release/SDL2-devel-2.0.12-VC.zip" \ + "https://gitlab.com/OpenMW/openmw-deps/-/raw/main/windows/SDL2-2.0.12.zip" \ "SDL2-2.0.12.zip" # Google test and mock @@ -525,7 +594,7 @@ elif [ -n "$NINJA" ]; then fi if [ -n "$SINGLE_CONFIG" ]; then - BUILD_DIR="${BUILD_DIR}_${BUILD_CONFIG}" + BUILD_DIR="${BUILD_DIR}_${CONFIGURATIONS[0]}" fi if [ -z $KEEP ]; then @@ -626,7 +695,9 @@ printf "FFmpeg 4.2.2... " rm -rf "ffmpeg-4.2.2-win${BITS}-dev" fi export FFMPEG_HOME="$(real_pwd)/FFmpeg" - add_runtime_dlls "$(pwd)/FFmpeg/bin/"{avcodec-58,avformat-58,avutil-56,swresample-3,swscale-5}.dll + 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 @@ -651,14 +722,16 @@ printf "MyGUI 3.4.0... " mv "MyGUI-3.4.0-msvc${MSVC_REAL_YEAR}-win${BITS}" MyGUI fi export MYGUI_HOME="$(real_pwd)/MyGUI" - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="_d" - MYGUI_CONFIGURATION="Debug" - else - SUFFIX="" - MYGUI_CONFIGURATION="RelWithDebInfo" - fi - add_runtime_dlls "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + if [ $CONFIGURATION == "Debug" ]; then + SUFFIX="_d" + MYGUI_CONFIGURATION="Debug" + else + SUFFIX="" + MYGUI_CONFIGURATION="RelWithDebInfo" + fi + add_runtime_dlls $CONFIGURATION "$(pwd)/MyGUI/bin/${MYGUI_CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" + done echo Done. } cd $DEPS @@ -675,7 +748,9 @@ printf "OpenAL-Soft 1.20.1... " OPENAL_SDK="$(real_pwd)/openal-soft-1.20.1-bin" add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \ -DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib" - add_runtime_dlls "$(pwd)/openal-soft-1.20.1-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" + for config in ${CONFIGURATIONS[@]}; do + add_runtime_dlls $config "$(pwd)/openal-soft-1.20.1-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" + done echo Done. } cd $DEPS @@ -698,15 +773,17 @@ printf "OSG 3.6.5... " fi OSG_SDK="$(real_pwd)/OSG" add_cmake_opts -DOSG_DIR="$OSG_SDK" - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="d" - else - SUFFIX="" - fi - add_runtime_dlls "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ - "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll - add_osg_dlls "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll - add_osg_dlls "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + if [ $CONFIGURATION == "Debug" ]; then + SUFFIX="d" + else + SUFFIX="" + fi + add_runtime_dlls $CONFIGURATION "$(pwd)/OSG/bin/"{OpenThreads,zlib,libpng}${SUFFIX}.dll \ + "$(pwd)/OSG/bin/osg"{,Animation,DB,FX,GA,Particle,Text,Util,Viewer,Shadow}${SUFFIX}.dll + add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_"{bmp,dds,freetype,jpeg,osg,png,tga}${SUFFIX}.dll + add_osg_dlls $CONFIGURATION "$(pwd)/OSG/bin/osgPlugins-3.6.5/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer,shadow}${SUFFIX}.dll + done echo Done. } cd $DEPS @@ -778,26 +855,30 @@ fi cd $QT_SDK add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="d" - else - SUFFIX="" - fi - add_runtime_dlls "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${SUFFIX}.dll - add_qt_platform_dlls "$(pwd)/plugins/platforms/qwindows${SUFFIX}.dll" + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + if [ $CONFIGURATION == "Debug" ]; then + DLLSUFFIX="d" + else + DLLSUFFIX="" + fi + add_runtime_dlls $CONFIGURATION "$(pwd)/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll + add_qt_platform_dlls $CONFIGURATION "$(pwd)/plugins/platforms/qwindows${DLLSUFFIX}.dll" + done echo Done. else QT_SDK="C:/Qt/5.13/msvc2017${SUFFIX}" add_cmake_opts -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" - if [ $CONFIGURATION == "Debug" ]; then - SUFFIX="d" - else - SUFFIX="" - fi - DIR=$(windowsPathAsUnix "${QT_SDK}") - add_runtime_dlls "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${SUFFIX}.dll - add_qt_platform_dlls "${DIR}/plugins/platforms/qwindows${SUFFIX}.dll" + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + if [ $CONFIGURATION == "Debug" ]; then + DLLSUFFIX="d" + else + DLLSUFFIX="" + fi + DIR=$(windowsPathAsUnix "${QT_SDK}") + add_runtime_dlls $CONFIGURATION "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${DLLSUFFIX}.dll + add_qt_platform_dlls $CONFIGURATION "${DIR}/plugins/platforms/qwindows${DLLSUFFIX}.dll" + done echo Done. fi } @@ -813,7 +894,9 @@ printf "SDL 2.0.12... " eval 7z x -y SDL2-2.0.12.zip $STRIP fi export SDL2DIR="$(real_pwd)/SDL2-2.0.12" - add_runtime_dlls "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll" + for config in ${CONFIGURATIONS[@]}; do + add_runtime_dlls $config "$(pwd)/SDL2-2.0.12/lib/x${ARCHSUFFIX}/SDL2.dll" + done echo Done. } cd $DEPS @@ -823,41 +906,51 @@ if [ ! -z $TEST_FRAMEWORK ]; then printf "Google test 1.10.0 ..." cd googletest - if [ ! -d build ]; then - mkdir build - fi + mkdir -p build${MSVC_REAL_YEAR} - cd build + cd build${MSVC_REAL_YEAR} GOOGLE_INSTALL_ROOT="${DEPS_INSTALL}/GoogleTest" - if [ $CONFIGURATION == "Debug" ]; then + + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + # FindGMock.cmake mentions Release explicitly, but not RelWithDebInfo. Only one optimised library config can be used, so go for the safer one. + GTEST_CONFIG=$([ $CONFIGURATION == "RelWithDebInfo" ] && echo "Release" || echo "$CONFIGURATION" ) + if [ $GTEST_CONFIG == "Debug" ]; then DEBUG_SUFFIX="d" else DEBUG_SUFFIX="" - fi + fi - if [ ! -d $GOOGLE_INSTALL_ROOT ]; then + if [ ! -f "$GOOGLE_INSTALL_ROOT/lib/gtest${DEBUG_SUFFIX}.lib" ]; then + # Always use MSBuild solution files as they don't need the environment activating + cmake .. -DCMAKE_USE_WIN32_THREADS_INIT=1 -G "Visual Studio $MSVC_REAL_VER $MSVC_REAL_YEAR$([ $BITS -eq 64 ] && [ $MSVC_REAL_VER -lt 16 ] && echo " Win64")" $([ $MSVC_REAL_VER -ge 16 ] && echo "-A $([ $BITS -eq 64 ] && echo "x64" || echo "Win32")") -DBUILD_SHARED_LIBS=1 + cmake --build . --config "${GTEST_CONFIG}" + cmake --install . --config "${GTEST_CONFIG}" --prefix "${GOOGLE_INSTALL_ROOT}" + fi - cmake .. -DCMAKE_BUILD_TYPE="${CONFIGURATION}" -DCMAKE_INSTALL_PREFIX="${GOOGLE_INSTALL_ROOT}" -DCMAKE_USE_WIN32_THREADS_INIT=1 -G "${GENERATOR}" -DBUILD_SHARED_LIBS=1 - cmake --build . --config "${CONFIGURATION}" - cmake --build . --target install --config "${CONFIGURATION}" - - add_runtime_dlls "${GOOGLE_INSTALL_ROOT}\bin\gtest_main${DEBUG_SUFFIX}.dll" - add_runtime_dlls "${GOOGLE_INSTALL_ROOT}\bin\gtest${DEBUG_SUFFIX}.dll" - add_runtime_dlls "${GOOGLE_INSTALL_ROOT}\bin\gmock_main${DEBUG_SUFFIX}.dll" - add_runtime_dlls "${GOOGLE_INSTALL_ROOT}\bin\gmock${DEBUG_SUFFIX}.dll" - fi + add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest_main${DEBUG_SUFFIX}.dll" + add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gtest${DEBUG_SUFFIX}.dll" + add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock_main${DEBUG_SUFFIX}.dll" + add_runtime_dlls $CONFIGURATION "${GOOGLE_INSTALL_ROOT}\bin\gmock${DEBUG_SUFFIX}.dll" + done add_cmake_opts -DBUILD_UNITTESTS=yes # FindGTest and FindGMock do not work perfectly on Windows # but we can help them by telling them everything we know about installation add_cmake_opts -DGMOCK_ROOT="$GOOGLE_INSTALL_ROOT" add_cmake_opts -DGTEST_ROOT="$GOOGLE_INSTALL_ROOT" - add_cmake_opts -DGTEST_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest${DEBUG_SUFFIX}.lib" - add_cmake_opts -DGTEST_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest_main${DEBUG_SUFFIX}.lib" - add_cmake_opts -DGMOCK_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock${DEBUG_SUFFIX}.lib" - add_cmake_opts -DGMOCK_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock_main${DEBUG_SUFFIX}.lib" + add_cmake_opts -DGTEST_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest.lib" + add_cmake_opts -DGTEST_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gtest_main.lib" + add_cmake_opts -DGMOCK_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock.lib" + add_cmake_opts -DGMOCK_MAIN_LIBRARY="$GOOGLE_INSTALL_ROOT/lib/gmock_main.lib" + add_cmake_opts -DGTEST_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtestd.lib" + add_cmake_opts -DGTEST_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gtest_maind.lib" + add_cmake_opts -DGMOCK_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmockd.lib" + add_cmake_opts -DGMOCK_MAIN_LIBRARY_DEBUG="$GOOGLE_INSTALL_ROOT/lib/gmock_maind.lib" add_cmake_opts -DGTEST_LINKED_AS_SHARED_LIBRARY=True + add_cmake_opts -DGTEST_LIBRARY_TYPE=SHARED + add_cmake_opts -DGTEST_MAIN_LIBRARY_TYPE=SHARED + echo Done. fi @@ -904,45 +997,47 @@ if [ ! -z $CI ]; then fi # NOTE: Disable this when/if we want to run test cases #if [ -z $CI ]; then - echo "- Copying Runtime DLLs..." - DLL_PREFIX="" - if [ -z $SINGLE_CONFIG ]; then - mkdir -p $BUILD_CONFIG - DLL_PREFIX="$BUILD_CONFIG/" - fi - for DLL in $RUNTIME_DLLS; do - TARGET="$(basename "$DLL")" - if [[ "$DLL" == *":"* ]]; then - originalIFS="$IFS" - IFS=':'; SPLIT=( ${DLL} ); IFS=$originalIFS - DLL=${SPLIT[0]} - TARGET=${SPLIT[1]} + for CONFIGURATION in ${CONFIGURATIONS[@]}; do + echo "- Copying Runtime DLLs for $CONFIGURATION..." + DLL_PREFIX="" + if [ -z $SINGLE_CONFIG ]; then + mkdir -p $CONFIGURATION + DLL_PREFIX="$CONFIGURATION/" fi - echo " ${TARGET}." - cp "$DLL" "${DLL_PREFIX}$TARGET" + for DLL in ${RUNTIME_DLLS[$CONFIGURATION]}; do + TARGET="$(basename "$DLL")" + if [[ "$DLL" == *":"* ]]; then + originalIFS="$IFS" + IFS=':'; SPLIT=( ${DLL} ); IFS=$originalIFS + DLL=${SPLIT[0]} + TARGET=${SPLIT[1]} + fi + echo " ${TARGET}." + cp "$DLL" "${DLL_PREFIX}$TARGET" + done + echo + echo "- OSG Plugin DLLs..." + mkdir -p ${DLL_PREFIX}osgPlugins-3.6.5 + for DLL in ${OSG_PLUGINS[$CONFIGURATION]}; do + echo " $(basename $DLL)." + cp "$DLL" ${DLL_PREFIX}osgPlugins-3.6.5 + done + echo + echo "- Qt Platform DLLs..." + mkdir -p ${DLL_PREFIX}platforms + for DLL in ${QT_PLATFORMS[$CONFIGURATION]}; do + echo " $(basename $DLL)" + cp "$DLL" "${DLL_PREFIX}platforms" + done + echo done - echo - echo "- OSG Plugin DLLs..." - mkdir -p ${DLL_PREFIX}osgPlugins-3.6.5 - for DLL in $OSG_PLUGINS; do - echo " $(basename $DLL)." - cp "$DLL" ${DLL_PREFIX}osgPlugins-3.6.5 - done - echo - echo "- Qt Platform DLLs..." - mkdir -p ${DLL_PREFIX}platforms - for DLL in $QT_PLATFORMS; do - echo " $(basename $DLL)" - cp "$DLL" "${DLL_PREFIX}platforms" - done - echo #fi if [ -n "$ACTIVATE_MSVC" ]; then echo -n "- Activating MSVC in the current shell... " command -v vswhere >/dev/null 2>&1 || { echo "Error: vswhere is not on the path."; wrappedExit 1; } - MSVC_INSTALLATION_PATH=$(vswhere -legacy -products '*' -version "[$MSVC_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath) + MSVC_INSTALLATION_PATH=$(vswhere -products '*' -version "[$MSVC_REAL_VER,$(awk "BEGIN { print $MSVC_REAL_VER + 1; exit }"))" -property installationPath) if [ -z "$MSVC_INSTALLATION_PATH" ]; then echo "vswhere was unable to find MSVC $MSVC_DISPLAY_YEAR" wrappedExit 1 diff --git a/CMakeLists.txt b/CMakeLists.txt index a185d7fb3..3340f3286 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,11 @@ project(OpenMW) cmake_minimum_required(VERSION 3.1.0) +# for link time optimization, remove if cmake version is >= 3.9 +if(POLICY CMP0069) + cmake_policy(SET CMP0069 NEW) +endif() + # Apps and tools option(BUILD_OPENMW "Build OpenMW" ON) option(BUILD_LAUNCHER "Build Launcher" ON) @@ -100,6 +105,7 @@ option(OSG_STATIC "Link static build of OpenSceneGraph into the binaries" FALSE) option(QT_STATIC "Link static build of QT into the binaries" FALSE) 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 ) @@ -113,7 +119,6 @@ option(OPENMW_OSX_DEPLOYMENT OFF) if (MSVC) option(OPENMW_MP_BUILD "Build OpenMW with /MP flag" OFF) - option(OPENMW_LTO_BUILD "Build OpenMW with Link-Time Optimization (Needs ~2GB of RAM)" OFF) endif() # Set up common paths @@ -411,6 +416,26 @@ endif() # CXX Compiler settings set(CMAKE_CXX_STANDARD 14) + +if(OPENMW_LTO_BUILD) + if(NOT CMAKE_VERSION VERSION_LESS 3.9) + include(CheckIPOSupported) + check_ipo_supported(RESULT HAVE_IPO OUTPUT HAVE_IPO_OUTPUT) + if(HAVE_IPO) + message(STATUS "LTO enabled for Release configuration.") + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE) + else() + message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this compiler: ${HAVE_IPO_OUTPUT}") + if(MSVC) + message(STATUS "Note: Flags used to be set manually for this setting with MSVC. We now rely on CMake for this. Upgrade CMake to at least 3.13 to re-enable this setting.") + endif() + endif() + else() + message(WARNING "Requested option OPENMW_LTO_BUILD not supported by this cmake version: ${CMAKE_VERSION}. Upgrade CMake to at least 3.9 to enable support for certain compilers. Newer CMake versions support more compilers.") + endif() +endif() + + if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wundef -Wno-unused-parameter -std=c++14 -pedantic -Wno-long-long") add_definitions( -DBOOST_NO_CXX11_SCOPED_ENUMS=ON ) @@ -430,14 +455,6 @@ if (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-but-set-parameter") endif() elseif (MSVC) - # Enable link-time code generation globally for all linking - if (OPENMW_LTO_BUILD) - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /GL") - set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG") - set(CMAKE_SHARED_LINKER_FLAGS_RELEASE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE} /LTCG") - set(CMAKE_STATIC_LINKER_FLAGS_RELEASE "${CMAKE_STATIC_LINKER_FLAGS_RELEASE} /LTCG") - endif() - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /FORCE:MULTIPLE") endif (CMAKE_CXX_COMPILER_ID STREQUAL GNU OR CMAKE_CXX_COMPILER_ID STREQUAL Clang) diff --git a/apps/launcher/advancedpage.cpp b/apps/launcher/advancedpage.cpp index bc14a9269..2e929faf5 100644 --- a/apps/launcher/advancedpage.cpp +++ b/apps/launcher/advancedpage.cpp @@ -88,6 +88,7 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); loadSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); loadSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); + loadSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); int unarmedFactorsStrengthIndex = mEngineSettings.getInt("strength influences hand to hand", "Game"); if (unarmedFactorsStrengthIndex >= 0 && unarmedFactorsStrengthIndex <= 2) unarmedFactorsStrengthComboBox->setCurrentIndex(unarmedFactorsStrengthIndex); @@ -112,6 +113,7 @@ bool Launcher::AdvancedPage::loadSettings() loadSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); } loadSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); + loadSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); @@ -200,6 +202,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(uncappedDamageFatigueCheckBox, "uncapped damage fatigue", "Game"); saveSettingBool(normaliseRaceSpeedCheckBox, "normalise race speed", "Game"); saveSettingBool(swimUpwardCorrectionCheckBox, "swim upward correction", "Game"); + saveSettingBool(avoidCollisionsCheckBox, "NPCs avoid collisions", "Game"); int unarmedFactorsStrengthIndex = unarmedFactorsStrengthComboBox->currentIndex(); if (unarmedFactorsStrengthIndex != mEngineSettings.getInt("strength influences hand to hand", "Game")) mEngineSettings.setInt("strength influences hand to hand", "Game", unarmedFactorsStrengthIndex); @@ -220,6 +223,7 @@ void Launcher::AdvancedPage::saveSettings() saveSettingBool(weaponSheathingCheckBox, "weapon sheathing", "Game"); saveSettingBool(shieldSheathingCheckBox, "shield sheathing", "Game"); saveSettingBool(turnToMovementDirectionCheckBox, "turn to movement direction", "Game"); + saveSettingBool(smoothMovementCheckBox, "smooth movement", "Game"); const bool distantTerrain = mEngineSettings.getBool("distant terrain", "Terrain"); const bool objectPaging = mEngineSettings.getBool("object paging", "Terrain"); diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 3a0eba06f..3ce4f1696 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -14,6 +14,7 @@ #include #include +#include #include @@ -612,7 +613,7 @@ void OMW::Engine::createWindow(Settings::Manager& settings) bool fullscreen = settings.getBool("fullscreen", "Video"); bool windowBorder = settings.getBool("window border", "Video"); bool vsync = settings.getBool("vsync", "Video"); - int antialiasing = settings.getInt("antialiasing", "Video"); + unsigned int antialiasing = std::max(0, settings.getInt("antialiasing", "Video")); int pos_x = SDL_WINDOWPOS_CENTERED_DISPLAY(screen), pos_y = SDL_WINDOWPOS_CENTERED_DISPLAY(screen); @@ -638,6 +639,8 @@ void OMW::Engine::createWindow(Settings::Manager& settings) checkSDLError(SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0)); checkSDLError(SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24)); + if (Debug::shouldDebugOpenGL()) + checkSDLError(SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG)); if (antialiasing > 0) { @@ -645,62 +648,80 @@ void OMW::Engine::createWindow(Settings::Manager& settings) checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); } - while (!mWindow) + osg::ref_ptr graphicsWindow; + while (!graphicsWindow || !graphicsWindow->valid()) { - mWindow = SDL_CreateWindow("OpenMW", pos_x, pos_y, width, height, flags); - if (!mWindow) + while (!mWindow) { - // Try with a lower AA - if (antialiasing > 0) + mWindow = SDL_CreateWindow("OpenMW", pos_x, pos_y, width, height, flags); + if (!mWindow) { - Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing/2; - antialiasing /= 2; - Settings::Manager::setInt("antialiasing", "Video", antialiasing); - checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); - continue; - } - else - { - std::stringstream error; - error << "Failed to create SDL window: " << SDL_GetError(); - throw std::runtime_error(error.str()); + // Try with a lower AA + if (antialiasing > 0) + { + Log(Debug::Warning) << "Warning: " << antialiasing << "x antialiasing not supported, trying " << antialiasing/2; + antialiasing /= 2; + Settings::Manager::setInt("antialiasing", "Video", antialiasing); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); + continue; + } + else + { + std::stringstream error; + error << "Failed to create SDL window: " << SDL_GetError(); + throw std::runtime_error(error.str()); + } } } + + setWindowIcon(); + + osg::ref_ptr traits = new osg::GraphicsContext::Traits; + SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); + SDL_GetWindowSize(mWindow, &traits->width, &traits->height); + traits->windowName = SDL_GetWindowTitle(mWindow); + traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); + traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); + traits->vsync = vsync; + traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); + + graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); + if (!graphicsWindow->valid()) throw std::runtime_error("Failed to create GraphicsContext"); + + if (traits->samples < antialiasing) + { + Log(Debug::Warning) << "Warning: Framebuffer MSAA level is only " << traits->samples << "x instead of " << antialiasing << "x. Trying " << antialiasing / 2 << "x instead."; + graphicsWindow->closeImplementation(); + SDL_DestroyWindow(mWindow); + mWindow = nullptr; + antialiasing /= 2; + Settings::Manager::setInt("antialiasing", "Video", antialiasing); + checkSDLError(SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, antialiasing)); + continue; + } + + if (traits->red < 8) + Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->red << " bit red channel."; + if (traits->green < 8) + Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->green << " bit green channel."; + if (traits->blue < 8) + Log(Debug::Warning) << "Warning: Framebuffer only has a " << traits->blue << " bit blue channel."; + if (traits->depth < 8) + Log(Debug::Warning) << "Warning: Framebuffer only has " << traits->red << " bits of depth precision."; + + traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel } - setWindowIcon(); - - osg::ref_ptr traits = new osg::GraphicsContext::Traits; - SDL_GetWindowPosition(mWindow, &traits->x, &traits->y); - SDL_GetWindowSize(mWindow, &traits->width, &traits->height); - traits->windowName = SDL_GetWindowTitle(mWindow); - traits->windowDecoration = !(SDL_GetWindowFlags(mWindow)&SDL_WINDOW_BORDERLESS); - traits->screenNum = SDL_GetWindowDisplayIndex(mWindow); - // We tried to get rid of the hardcoding but failed: https://github.com/OpenMW/openmw/pull/1771 - // Here goes kcat's quote: - // It's ultimately a chicken and egg problem, and the reason why the code is like it was in the first place. - // It needs a context to get the current attributes, but it needs the attributes to set up the context. - // So it just specifies the same values that were given to SDL in the hopes that it's good enough to what the window eventually gets. - traits->red = 8; - traits->green = 8; - traits->blue = 8; - traits->alpha = 0; // set to 0 to stop ScreenCaptureHandler reading the alpha channel - traits->depth = 24; - traits->stencil = 8; - traits->vsync = vsync; - traits->doubleBuffer = true; - traits->inheritedWindowData = new SDLUtil::GraphicsWindowSDL2::WindowData(mWindow); - - osg::ref_ptr graphicsWindow = new SDLUtil::GraphicsWindowSDL2(traits); - if(!graphicsWindow->valid()) throw std::runtime_error("Failed to create GraphicsContext"); - osg::ref_ptr camera = mViewer->getCamera(); camera->setGraphicsContext(graphicsWindow); - camera->setViewport(0, 0, traits->width, traits->height); + camera->setViewport(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); + + if (Debug::shouldDebugOpenGL()) + mViewer->setRealizeOperation(new Debug::EnableGLDebugOperation()); mViewer->realize(); - mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, traits->width, traits->height); + mViewer->getEventQueue()->getCurrentEventState()->setWindowRectangle(0, 0, graphicsWindow->getTraits()->width, graphicsWindow->getTraits()->height); } void OMW::Engine::setWindowIcon() diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index dcf24e104..1a1f98d65 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -1161,6 +1161,8 @@ namespace MWClass float Npc::getMaxSpeed(const MWWorld::Ptr& ptr) const { + // TODO: This function is called several times per frame for each NPC. + // It would be better to calculate it only once per frame for each NPC and save the result in CreatureStats. const MWMechanics::CreatureStats& stats = getCreatureStats(ptr); if (stats.isParalyzed() || stats.getKnockedDown() || stats.isDead()) return 0.f; diff --git a/apps/openmw/mwgui/container.cpp b/apps/openmw/mwgui/container.cpp index dd11bf208..c6b213c9c 100644 --- a/apps/openmw/mwgui/container.cpp +++ b/apps/openmw/mwgui/container.cpp @@ -405,7 +405,7 @@ namespace MWGui } // Clean up summoned creatures as well - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + std::map& creatureMap = creatureStats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mPtr, creature.second); creatureMap.clear(); diff --git a/apps/openmw/mwgui/merchantrepair.cpp b/apps/openmw/mwgui/merchantrepair.cpp index 16f9270aa..3f857668f 100644 --- a/apps/openmw/mwgui/merchantrepair.cpp +++ b/apps/openmw/mwgui/merchantrepair.cpp @@ -75,6 +75,7 @@ void MerchantRepair::setPtr(const MWWorld::Ptr &actor) int x = static_cast((maxDurability - durability) / r); x = static_cast(fRepairMult * x); + x = std::max(1, x); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mActor, x, true); diff --git a/apps/openmw/mwgui/spellbuyingwindow.cpp b/apps/openmw/mwgui/spellbuyingwindow.cpp index 5440bc92f..81fedbcc3 100644 --- a/apps/openmw/mwgui/spellbuyingwindow.cpp +++ b/apps/openmw/mwgui/spellbuyingwindow.cpp @@ -55,7 +55,7 @@ namespace MWGui const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); - int price = static_cast(spell.mData.mCost*store.get().find("fSpellValueMult")->mValue.getFloat()); + int price = std::max(1, static_cast(spell.mData.mCost*store.get().find("fSpellValueMult")->mValue.getFloat())); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr,price,true); MWWorld::Ptr player = MWMechanics::getPlayer(); diff --git a/apps/openmw/mwgui/spellcreationdialog.cpp b/apps/openmw/mwgui/spellcreationdialog.cpp index 883615163..514629ad9 100644 --- a/apps/openmw/mwgui/spellcreationdialog.cpp +++ b/apps/openmw/mwgui/spellcreationdialog.cpp @@ -517,7 +517,8 @@ namespace MWGui float fSpellMakingValueMult = store.get().find("fSpellMakingValueMult")->mValue.getFloat(); - int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, static_cast(y * fSpellMakingValueMult),true); + int price = std::max(1, static_cast(y * fSpellMakingValueMult)); + price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); mPriceLabel->setCaption(MyGUI::utility::toString(int(price))); @@ -780,5 +781,24 @@ namespace MWGui { mAddEffectDialog.setConstantEffect(constant); mConstantEffect = constant; + + if (!constant) + return; + + for (auto it = mEffects.begin(); it != mEffects.end();) + { + if (it->mRange != ESM::RT_Self) + { + auto& store = MWBase::Environment::get().getWorld()->getStore(); + auto magicEffect = store.get().find(it->mEffectID); + if ((magicEffect->mData.mFlags & ESM::MagicEffect::CastSelf) == 0) + { + it = mEffects.erase(it); + continue; + } + it->mRange = ESM::RT_Self; + } + ++it; + } } } diff --git a/apps/openmw/mwgui/spellicons.cpp b/apps/openmw/mwgui/spellicons.cpp index 8a501e598..e6a10ee32 100644 --- a/apps/openmw/mwgui/spellicons.cpp +++ b/apps/openmw/mwgui/spellicons.cpp @@ -25,8 +25,8 @@ namespace MWGui { - void EffectSourceVisitor::visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + void EffectSourceVisitor::visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) { MagicEffectInfo newEffectSource; diff --git a/apps/openmw/mwgui/spellicons.hpp b/apps/openmw/mwgui/spellicons.hpp index 26761f2fc..67351688f 100644 --- a/apps/openmw/mwgui/spellicons.hpp +++ b/apps/openmw/mwgui/spellicons.hpp @@ -46,8 +46,8 @@ namespace MWGui virtual ~EffectSourceVisitor() {} - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1); }; diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index 7672824fe..910ba67d1 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -111,8 +111,9 @@ namespace MWGui for (int i=0; i<3; ++i) { - int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer - (mPtr,pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->mValue.getInteger(),true); + int price = static_cast(pcStats.getSkill (skills[i].first).getBase() * gmst.find("iTrainingMod")->mValue.getInteger()); + price = std::max(1, price); + price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); MyGUI::Button* button = mTrainingOptions->createWidget(price <= playerGold ? "SandTextButton" : "SandTextButtonDisabled", // can't use setEnabled since that removes tooltip MyGUI::IntCoord(5, 5+i*18, mTrainingOptions->getWidth()-10, 18), MyGUI::Align::Default); diff --git a/apps/openmw/mwgui/travelwindow.cpp b/apps/openmw/mwgui/travelwindow.cpp index daf4e8764..442bf28c5 100644 --- a/apps/openmw/mwgui/travelwindow.cpp +++ b/apps/openmw/mwgui/travelwindow.cpp @@ -74,9 +74,14 @@ namespace MWGui { ESM::Position PlayerPos = player.getRefData().getPosition(); float d = sqrt(pow(pos.pos[0] - PlayerPos.pos[0], 2) + pow(pos.pos[1] - PlayerPos.pos[1], 2) + pow(pos.pos[2] - PlayerPos.pos[2], 2)); - price = static_cast(d / gmst.find("fTravelMult")->mValue.getFloat()); + float fTravelMult = gmst.find("fTravelMult")->mValue.getFloat(); + if (fTravelMult != 0) + price = static_cast(d / fTravelMult); + else + price = static_cast(d); } + price = std::max(1, price); price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mPtr, price, true); // Add price for the travelling followers diff --git a/apps/openmw/mwinput/actionmanager.cpp b/apps/openmw/mwinput/actionmanager.cpp index bf7a6db18..37ab5738c 100644 --- a/apps/openmw/mwinput/actionmanager.cpp +++ b/apps/openmw/mwinput/actionmanager.cpp @@ -152,7 +152,7 @@ namespace MWInput float xAxis = mBindingsManager->getActionValue(A_MoveLeftRight); float yAxis = mBindingsManager->getActionValue(A_MoveForwardBackward); - bool isRunning = xAxis > .75 || xAxis < .25 || yAxis > .75 || yAxis < .25; + bool isRunning = osg::Vec2f(xAxis * 2 - 1, yAxis * 2 - 1).length2() > 0.25f; if ((mAlwaysRunActive && alwaysRunAllowed) || isRunning) player.setRunState(!mBindingsManager->actionIsActive(A_Run)); else diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index dbdfee991..114bd88f4 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -252,9 +252,8 @@ namespace MWMechanics std::string name = it->second.mDisplayName; float magnitude = effectIt->mMagnitude; - if (magnitude) - visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); + visitor.visit(MWMechanics::EffectKey(effectIt->mEffectId, effectIt->mArg), effectIt->mEffectIndex, name, it->first, it->second.mCasterActorId, magnitude, effectIt->mTimeLeft, effectIt->mDuration); } } } @@ -300,14 +299,14 @@ namespace MWMechanics mSpellsChanged = true; } - void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId) + void ActiveSpells::purgeEffect(short effectId, const std::string& sourceId, int effectIndex) { for (TContainer::iterator it = mSpells.begin(); it != mSpells.end(); ++it) { for (std::vector::iterator effectIt = it->second.mEffects.begin(); effectIt != it->second.mEffects.end();) { - if (effectIt->mEffectId == effectId && it->first == sourceId) + if (effectIt->mEffectId == effectId && it->first == sourceId && (effectIndex < 0 || effectIndex == effectIt->mEffectIndex)) effectIt = it->second.mEffects.erase(effectIt); else ++effectIt; diff --git a/apps/openmw/mwmechanics/activespells.hpp b/apps/openmw/mwmechanics/activespells.hpp index a48b89778..b5baeee0e 100644 --- a/apps/openmw/mwmechanics/activespells.hpp +++ b/apps/openmw/mwmechanics/activespells.hpp @@ -85,7 +85,7 @@ namespace MWMechanics void purgeEffect (short effectId); /// Remove all active effects with this effect id and source id - void purgeEffect (short effectId, const std::string& sourceId); + void purgeEffect (short effectId, const std::string& sourceId, int effectIndex=-1); /// Remove all active effects, if roll succeeds (for each effect) void purgeAll(float chance, bool spellOnly = false); diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index 1772ad980..20b3e5fcd 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include /* @@ -55,6 +56,7 @@ #include "aicombataction.hpp" #include "aifollow.hpp" #include "aipursue.hpp" +#include "aiwander.hpp" #include "actor.hpp" #include "summoning.hpp" #include "combat.hpp" @@ -107,12 +109,13 @@ class CheckActorCommanded : public MWMechanics::EffectSourceVisitor MWWorld::Ptr mActor; public: bool mCommanded; + CheckActorCommanded(const MWWorld::Ptr& actor) : mActor(actor) - , mCommanded(false){} + , mCommanded(false){} - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) { if (((key.mId == ESM::MagicEffect::CommandHumanoid && mActor.getClass().isNpc()) @@ -175,8 +178,8 @@ namespace MWMechanics GetStuntedMagickaDuration(const MWWorld::Ptr& actor) : mRemainingTime(0.f){} - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) { if (mRemainingTime == -1) return; @@ -205,8 +208,8 @@ namespace MWMechanics { } - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) { if (magnitude <= 0) @@ -225,8 +228,8 @@ namespace MWMechanics { public: - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) { if (key.mId != ESM::MagicEffect::Corprus) @@ -250,8 +253,8 @@ namespace MWMechanics { } - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) { if (mTrapped) @@ -456,7 +459,7 @@ namespace MWMechanics const osg::Vec3f actor2Pos(targetActor.getRefData().getPosition().asVec3()); float sqrDist = (actor1Pos - actor2Pos).length2(); - if (sqrDist > maxDistance*maxDistance) + if (sqrDist > std::min(maxDistance * maxDistance, sqrHeadTrackDistance)) return; // stop tracking when target is behind the actor @@ -464,10 +467,7 @@ namespace MWMechanics osg::Vec3f targetDirection(actor2Pos - actor1Pos); actorDirection.z() = 0; targetDirection.z() = 0; - actorDirection.normalize(); - targetDirection.normalize(); - if (std::acos(actorDirection * targetDirection) < osg::DegreesToRadians(90.f) - && sqrDist <= sqrHeadTrackDistance + if (actorDirection * targetDirection > 0 && MWBase::Environment::get().getWorld()->getLOS(actor, targetActor) // check LOS and awareness last as it's the most expensive function && MWBase::Environment::get().getMechanicsManager()->awarenessCheck(targetActor, actor)) { @@ -505,6 +505,9 @@ namespace MWMechanics void Actors::updateMovementSpeed(const MWWorld::Ptr& actor) { + if (mSmoothMovement) + return; + CreatureStats &stats = actor.getClass().getCreatureStats(actor); MWMechanics::AiSequence& seq = stats.getAiSequence(); @@ -513,9 +516,10 @@ namespace MWMechanics osg::Vec3f targetPos = seq.getActivePackage().getDestination(); osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); float distance = (targetPos - actorPos).length(); + if (distance < DECELERATE_DISTANCE) { - float speedCoef = std::max(0.7f, 0.1f * (distance/64.f + 2.f)); + float speedCoef = std::max(0.7f, 0.2f + 0.8f * distance / DECELERATE_DISTANCE); auto& movement = actor.getClass().getMovementSettings(actor); movement.mPosition[0] *= speedCoef; movement.mPosition[1] *= speedCoef; @@ -619,8 +623,11 @@ namespace MWMechanics if (!actorState.isTurningToPlayer()) { - actorState.setAngleToPlayer(std::atan2(dir.x(), dir.y())); - actorState.setTurningToPlayer(true); + float angle = std::atan2(dir.x(), dir.y()); + actorState.setAngleToPlayer(angle); + float deltaAngle = Misc::normalizeAngle(angle - actor.getRefData().getPosition().rot[2]); + if (!mSmoothMovement || std::abs(deltaAngle) > osg::DegreesToRadians(60.f)) + actorState.setTurningToPlayer(true); } } @@ -939,7 +946,7 @@ namespace MWMechanics { } - virtual void visit (MWMechanics::EffectKey key, + virtual void visit (MWMechanics::EffectKey key, int /*effectIndex*/, const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float magnitude, float remainingTime = -1, float /*totalTime*/ = -1) { @@ -1260,6 +1267,7 @@ namespace MWMechanics { UpdateSummonedCreatures updateSummonedCreatures(ptr); creatureStats.getActiveSpells().visitEffectSources(updateSummonedCreatures); + creatureStats.getSpells().visitEffectSources(updateSummonedCreatures); if (ptr.getClass().hasInventoryStore(ptr)) ptr.getClass().getInventoryStore(ptr).visitEffectSources(updateSummonedCreatures); updateSummonedCreatures.process(mTimerDisposeSummonsCorpses == 0.f); @@ -1568,7 +1576,7 @@ namespace MWMechanics } } - Actors::Actors() + Actors::Actors() : mSmoothMovement(Settings::Manager::getBool("smooth movement", "Game")) { mTimerDisposeSummonsCorpses = 0.2f; // We should add a delay between summoned creature death and its corpse despawning @@ -1778,6 +1786,131 @@ namespace MWMechanics } + void Actors::predictAndAvoidCollisions() + { + const float minGap = 10.f; + const float maxDistToCheck = 100.f; + const float maxTimeToCheck = 1.f; + static const bool giveWayWhenIdle = Settings::Manager::getBool("NPCs give way", "Game"); + + MWWorld::Ptr player = getPlayer(); + MWBase::World* world = MWBase::Environment::get().getWorld(); + for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) + { + const MWWorld::Ptr& ptr = iter->first; + if (ptr == player) + continue; // Don't interfere with player controls. + + Movement& movement = ptr.getClass().getMovementSettings(ptr); + osg::Vec2f origMovement(movement.mPosition[0], movement.mPosition[1]); + bool isMoving = origMovement.length2() > 0.01; + + // Moving NPCs always should avoid collisions. + // Standing NPCs give way to moving ones if they are not in combat (or pursue) mode and either + // follow player or have a AIWander package with non-empty wander area. + bool shouldAvoidCollision = isMoving; + bool shouldTurnToApproachingActor = !isMoving; + MWWorld::Ptr currentTarget; // Combat or pursue target (NPCs should not avoid collision with their targets). + for (const auto& package : ptr.getClass().getCreatureStats(ptr).getAiSequence()) + { + if (package->getTypeId() == AiPackageTypeId::Follow) + shouldAvoidCollision = true; + else if (package->getTypeId() == AiPackageTypeId::Wander && giveWayWhenIdle) + { + if (!dynamic_cast(package.get())->isStationary()) + shouldAvoidCollision = true; + } + else if (package->getTypeId() == AiPackageTypeId::Combat || package->getTypeId() == AiPackageTypeId::Pursue) + { + currentTarget = package->getTarget(); + shouldAvoidCollision = isMoving; + shouldTurnToApproachingActor = false; + break; + } + } + if (!shouldAvoidCollision) + continue; + + float maxSpeed = ptr.getClass().getMaxSpeed(ptr); + osg::Vec2f baseSpeed = origMovement * maxSpeed; + osg::Vec3f basePos = ptr.getRefData().getPosition().asVec3(); + float baseRotZ = ptr.getRefData().getPosition().rot[2]; + osg::Vec3f halfExtents = world->getHalfExtents(ptr); + + float timeToCollision = maxTimeToCheck; + osg::Vec2f movementCorrection(0, 0); + float angleToApproachingActor = 0; + + // Iterate through all other actors and predict collisions. + for(PtrActorMap::iterator otherIter(mActors.begin()); otherIter != mActors.end(); ++otherIter) + { + const MWWorld::Ptr& otherPtr = otherIter->first; + if (otherPtr == ptr || otherPtr == currentTarget) + continue; + + osg::Vec3f otherHalfExtents = world->getHalfExtents(otherPtr); + osg::Vec3f deltaPos = otherPtr.getRefData().getPosition().asVec3() - basePos; + osg::Vec2f relPos = Misc::rotateVec2f(osg::Vec2f(deltaPos.x(), deltaPos.y()), baseRotZ); + + // Ignore actors which are not close enough or come from behind. + if (deltaPos.length2() > maxDistToCheck * maxDistToCheck || relPos.y() < 0) + continue; + + // Don't check for a collision if vertical distance is greater then the actor's height. + if (deltaPos.z() > halfExtents.z() * 2 || deltaPos.z() < -otherHalfExtents.z() * 2) + continue; + + osg::Vec3f speed = otherPtr.getClass().getMovementSettings(otherPtr).asVec3() * + otherPtr.getClass().getMaxSpeed(otherPtr); + float rotZ = otherPtr.getRefData().getPosition().rot[2]; + osg::Vec2f relSpeed = Misc::rotateVec2f(osg::Vec2f(speed.x(), speed.y()), baseRotZ - rotZ) - baseSpeed; + + float collisionDist = minGap + world->getHalfExtents(ptr).x() + world->getHalfExtents(otherPtr).x(); + collisionDist = std::min(collisionDist, relPos.length()); + + // Find the earliest `t` when |relPos + relSpeed * t| == collisionDist. + float vr = relPos.x() * relSpeed.x() + relPos.y() * relSpeed.y(); + float v2 = relSpeed.length2(); + float Dh = vr * vr - v2 * (relPos.length2() - collisionDist * collisionDist); + if (Dh <= 0 || v2 == 0) + continue; // No solution; distance is always >= collisionDist. + float t = (-vr - std::sqrt(Dh)) / v2; + + if (t < 0 || t > timeToCollision) + continue; + + // Check visibility and awareness last as it's expensive. + if (!MWBase::Environment::get().getWorld()->getLOS(otherPtr, ptr)) + continue; + if (!MWBase::Environment::get().getMechanicsManager()->awarenessCheck(otherPtr, ptr)) + continue; + + timeToCollision = t; + angleToApproachingActor = std::atan2(deltaPos.x(), deltaPos.y()); + osg::Vec2f posAtT = relPos + relSpeed * t; + float coef = (posAtT.x() * relSpeed.x() + posAtT.y() * relSpeed.y()) / (collisionDist * maxSpeed); + movementCorrection = posAtT * coef; + // Step to the side rather than backward. Otherwise player will be able to push the NPC far away from it's original location. + movementCorrection.y() = std::max(0.f, movementCorrection.y()); + } + + if (timeToCollision < maxTimeToCheck) + { + // Try to evade the nearest collision. + osg::Vec2f newMovement = origMovement + movementCorrection; + if (isMoving) + { // Keep the original speed. + newMovement.normalize(); + newMovement *= origMovement.length(); + } + movement.mPosition[0] = newMovement.x(); + movement.mPosition[1] = newMovement.y(); + if (shouldTurnToApproachingActor) + zTurn(ptr, angleToApproachingActor); + } + } + } + void Actors::update (float duration, bool paused) { if(!paused) @@ -1939,14 +2072,12 @@ namespace MWMechanics MWMechanics::CreatureStats& stats = iter->first.getClass().getCreatureStats(iter->first); bool firstPersonPlayer = isPlayer && world->isFirstPerson(); + bool inCombatOrPursue = stats.getAiSequence().isInCombat() || stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue); // 1. Unconsious actor can not track target // 2. Actors in combat and pursue mode do not bother to headtrack // 3. Player character does not use headtracking in the 1st-person view - if (!stats.getKnockedDown() && - !stats.getAiSequence().isInCombat() && - !stats.getAiSequence().hasPackage(AiPackageTypeId::Pursue) && - !firstPersonPlayer) + if (!stats.getKnockedDown() && !firstPersonPlayer && !inCombatOrPursue) { for(PtrActorMap::iterator it(mActors.begin()); it != mActors.end(); ++it) { @@ -1956,6 +2087,17 @@ namespace MWMechanics } } + if (!stats.getKnockedDown() && !isPlayer && inCombatOrPursue) + { + // Actors in combat and pursue mode always look at their target. + for (const auto& package : stats.getAiSequence()) + { + headTrackTarget = package->getTarget(); + if (!headTrackTarget.isEmpty()) + break; + } + } + ctrl->setHeadTrackTarget(headTrackTarget); } @@ -1997,6 +2139,10 @@ namespace MWMechanics } } + static const bool avoidCollisions = Settings::Manager::getBool("NPCs avoid collisions", "Game"); + if (avoidCollisions) + predictAndAvoidCollisions(); + timerUpdateAITargets += duration; timerUpdateHeadTrack += duration; timerUpdateEquippedLight += duration; @@ -2233,7 +2379,7 @@ namespace MWMechanics // Remove the summoned creature's summoned creatures as well MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); - std::map& creatureMap = stats.getSummonedCreatureMap(); + std::map& creatureMap = stats.getSummonedCreatureMap(); for (const auto& creature : creatureMap) cleanupSummonedCreature(stats, creature.second); creatureMap.clear(); @@ -2270,10 +2416,11 @@ namespace MWMechanics for(PtrActorMap::iterator iter(mActors.begin()); iter != mActors.end(); ++iter) { - iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); - if (iter->first.getClass().getCreatureStats(iter->first).isDead()) + { + iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); continue; + } if (!sleep || iter->first == player) restoreDynamicStats(iter->first, hours, sleep); @@ -2290,13 +2437,14 @@ namespace MWMechanics if (iter->first.getClass().isNpc()) calculateNpcStatModifiers(iter->first, duration); + iter->first.getClass().getCreatureStats(iter->first).getActiveSpells().update(duration); + MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(iter->first); if (animation) { animation->removeEffects(); MWBase::Environment::get().getWorld()->applyLoopingParticles(iter->first); } - } fastForwardAi(); diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 668f740fb..e355f33c5 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -63,6 +63,8 @@ namespace MWMechanics void purgeSpellEffects (int casterActorId); + void predictAndAvoidCollisions(); + public: Actors(); @@ -229,6 +231,7 @@ namespace MWMechanics float mTimerDisposeSummonsCorpses; float mActorsProcessingRange; + bool mSmoothMovement; }; } diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp index 275a3a814..490dc119a 100644 --- a/apps/openmw/mwmechanics/actorutil.hpp +++ b/apps/openmw/mwmechanics/actorutil.hpp @@ -58,7 +58,7 @@ namespace MWMechanics template void modifyBaseInventory(const std::string& actorId, const std::string& itemId, int amount) { - ESM::NPC copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); + T copy = *MWBase::Environment::get().getWorld()->getStore().get().find(actorId); for(auto& it : copy.mInventory.mList) { if(Misc::StringUtils::ciEqual(it.mItem, itemId)) diff --git a/apps/openmw/mwmechanics/aibreathe.cpp b/apps/openmw/mwmechanics/aibreathe.cpp index 15251e125..2740355b5 100644 --- a/apps/openmw/mwmechanics/aibreathe.cpp +++ b/apps/openmw/mwmechanics/aibreathe.cpp @@ -23,7 +23,7 @@ bool MWMechanics::AiBreathe::execute (const MWWorld::Ptr& actor, CharacterContro actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getMovementSettings(actor).mPosition[1] = 1; - smoothTurn(actor, -180, 0); + smoothTurn(actor, -osg::PI / 2, 0); return false; } diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 500642572..98efec742 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -5,6 +5,10 @@ #include +#include + +#include + /* Start of tes3mp addition @@ -20,8 +24,6 @@ End of tes3mp addition */ -#include - #include "../mwphysics/collisiontype.hpp" #include "../mwworld/class.hpp" @@ -308,10 +310,6 @@ namespace MWMechanics if (storage.mReadyToAttack) { - storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); - // start new attack - storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); - if (isRangedCombat) { // rotate actor taking into account target movement direction and projectile speed @@ -327,6 +325,10 @@ namespace MWMechanics storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir); storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated } + + storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); + // start new attack + storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); } return false; } @@ -440,9 +442,13 @@ namespace MWMechanics void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage) { // apply combat movement + float deltaAngle = storage.mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]; + osg::Vec2f movement = Misc::rotateVec2f( + osg::Vec2f(storage.mMovement.mPosition[0], storage.mMovement.mPosition[1]), -deltaAngle); + MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); - actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0]; - actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1]; + actorMovementSettings.mPosition[0] = movement.x(); + actorMovementSettings.mPosition[1] = movement.y(); actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2]; rotateActorOnAxis(actor, 2, actorMovementSettings, storage); @@ -453,26 +459,11 @@ namespace MWMechanics MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage) { actorMovementSettings.mRotation[axis] = 0; - float& targetAngleRadians = storage.mMovement.mRotation[axis]; - if (targetAngleRadians != 0) - { - // Some attack animations contain small amount of movement. - // Since we use cone shapes for melee, we can use a threshold to avoid jittering - std::shared_ptr& currentAction = storage.mCurrentAction; - bool isRangedCombat = false; - currentAction->getCombatRange(isRangedCombat); - // Check if the actor now facing desired direction, no need to turn any more - if (isRangedCombat) - { - if (smoothTurn(actor, targetAngleRadians, axis)) - targetAngleRadians = 0; - } - else - { - if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f))) - targetAngleRadians = 0; - } - } + bool isRangedCombat = false; + storage.mCurrentAction->getCombatRange(isRangedCombat); + float eps = isRangedCombat ? osg::DegreesToRadians(0.5) : osg::DegreesToRadians(3.f); + float targetAngleRadians = storage.mMovement.mRotation[axis]; + smoothTurn(actor, targetAngleRadians, axis, eps); } MWWorld::Ptr AiCombat::getTarget() const @@ -557,12 +548,19 @@ namespace MWMechanics // Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff else if (actor.getClass().isBipedal(actor)) { - // apply sideway movement (kind of dodging) with some probability - // if actor is within range of target's weapon - if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) + float moveDuration = 0; + float angleToTarget = Misc::normalizeAngle(mMovement.mRotation[2] - actor.getRefData().getPosition().rot[2]); + // Apply a big side step if enemy tries to get around and come from behind. + // Otherwise apply a random side step (kind of dodging) with some probability + // if actor is within range of target's weapon. + if (std::abs(angleToTarget) > osg::PI / 4) + moveDuration = 0.2; + else if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) + moveDuration = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + if (moveDuration > 0) { mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right - mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); + mTimerCombatMove = moveDuration; mCombatMove = true; } } diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 53e366579..4bffd28ba 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" @@ -87,6 +88,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& //... But AI processing distance may increase in the future. if (isNearInactiveCell(position)) { + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; world->updateActorPath(actor, mPathFinder.getPath(), halfExtents, position, dest); return false; @@ -169,12 +171,34 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f& } // turn to next path point by X,Z axes - zTurn(actor, mPathFinder.getZAngleToNext(position.x(), position.y())); + float zAngleToNext = mPathFinder.getZAngleToNext(position.x(), position.y()); + zTurn(actor, zAngleToNext); smoothTurn(actor, mPathFinder.getXAngleToNext(position.x(), position.y(), position.z()), 0); const auto destination = mPathFinder.getPath().empty() ? dest : mPathFinder.getPath().front(); mObstacleCheck.update(actor, destination, duration); + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement) + { + const float smoothTurnReservedDist = 150; + auto& movement = actor.getClass().getMovementSettings(actor); + float distToNextSqr = osg::Vec2f(destination.x() - position.x(), destination.y() - position.y()).length2(); + float diffAngle = zAngleToNext - actor.getRefData().getPosition().rot[2]; + if (std::cos(diffAngle) < -0.1) + movement.mPosition[0] = movement.mPosition[1] = 0; + else if (distToNextSqr > smoothTurnReservedDist * smoothTurnReservedDist) + { // Go forward (and slowly turn towards the next path point) + movement.mPosition[0] = 0; + movement.mPosition[1] = 1; + } + else + { // Next path point is near, so use diagonal movement to follow the path precisely. + movement.mPosition[0] = std::sin(diffAngle); + movement.mPosition[1] = std::max(std::cos(diffAngle), 0.f); + } + } + // handle obstacles on the way evadeObstacles(actor); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index 9e179edeb..375209a25 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -27,7 +27,7 @@ namespace MWMechanics { static const int COUNT_BEFORE_RESET = 10; - static const float DOOR_CHECK_INTERVAL = 1.5f; + static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f; // to prevent overcrowding static const int DESTINATION_TOLERANCE = 64; @@ -96,6 +96,7 @@ namespace MWMechanics void stopMovement(const MWWorld::Ptr& actor) { + actor.getClass().getMovementSettings(actor).mPosition[0] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } @@ -424,15 +425,14 @@ namespace MWMechanics void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { - // Check if an idle actor is too close to a door - if so start walking - storage.mDoorCheckDuration += duration; + // Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. + storage.mCheckIdlePositionTimer += duration; - if (storage.mDoorCheckDuration >= DOOR_CHECK_INTERVAL) + if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) { - storage.mDoorCheckDuration = 0; // restart timer - static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance(); - if (mDistance && // actor is not intended to be stationary - proximityToDoor(actor, distance*1.6f)) + storage.mCheckIdlePositionTimer = 0; // restart timer + static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; + if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) { storage.setState(AiWanderStorage::Wander_MoveNow); storage.mTrimCurrentNode = false; // just in case @@ -451,6 +451,20 @@ namespace MWMechanics } } + bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const + { + const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); + auto cell = actor.getCell()->getCell(); + for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) + { + osg::Vec3f point(node.mX, node.mY, node.mZ); + Misc::CoordinateConverter(cell).toWorld(point); + if ((actorPos - point).length2() < distance * distance) + return true; + } + return false; + } + void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) { // Is there no destination or are we there yet? @@ -468,6 +482,9 @@ namespace MWMechanics void AiWander::onChooseActionStatePerFrameActions(const MWWorld::Ptr& actor, AiWanderStorage& storage) { + // Wait while fully stop before starting idle animation (important if "smooth movement" is enabled). + if (actor.getClass().getCurrentSpeed(actor) > 0) + return; unsigned short idleAnimation = getRandomIdle(); storage.mIdleAnimation = idleAnimation; diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index 4138c3dea..4165cebbd 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -53,7 +53,7 @@ namespace MWMechanics ESM::Pathgrid::Point mCurrentNode; bool mTrimCurrentNode; - float mDoorCheckDuration; + float mCheckIdlePositionTimer; int mStuckCount; AiWanderStorage(): @@ -66,7 +66,7 @@ namespace MWMechanics mPopulateAvailableNodes(true), mAllowedNodes(), mTrimCurrentNode(false), - mDoorCheckDuration(0), // TODO: maybe no longer needed + mCheckIdlePositionTimer(0), mStuckCount(0) {}; @@ -117,6 +117,8 @@ namespace MWMechanics return mDestination; } + bool isStationary() const { return mDistance == 0; } + private: void stopWalking(const MWWorld::Ptr& actor); @@ -137,6 +139,7 @@ namespace MWMechanics void wanderNearStart(const MWWorld::Ptr &actor, AiWanderStorage &storage, int wanderDistance); bool destinationIsAtWater(const MWWorld::Ptr &actor, const osg::Vec3f& destination); void completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage); + bool isNearAllowedNode(const MWWorld::Ptr &actor, const AiWanderStorage& storage, float distance) const; const int mDistance; // how far the actor can wander from the spawn point const int mDuration; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 0c10f3ca9..f690915ef 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1414,6 +1414,8 @@ bool CharacterController::updateWeaponState(CharacterState& idle) 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperCharState_UnEquipingWeap; + mAnimation->detachArrow(); + // If we do not have the "unequip detach" key, hide weapon manually. if (mAnimation->getTextKeyTime(weapgroup+": unequip detach") < 0) mAnimation->showWeapons(false); @@ -1946,7 +1948,9 @@ bool CharacterController::updateWeaponState(CharacterState& idle) mUpperBodyState = UpperCharState_MinAttackToMaxAttack; break; } - playSwishSound(0.0f); + + if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) + playSwishSound(0.0f); } if(mAttackType == "shoot") @@ -2141,22 +2145,65 @@ void CharacterController::update(float duration, bool animationOnly) End of tes3mp addition */ - if (isPlayer) - { - // TODO: Move this code to mwinput. - // Joystick analogue movement. - movementSettings.mSpeedFactor = std::max(std::abs(vec.x()), std::abs(vec.y())); - - // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used. - if(!isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) - movementSettings.mSpeedFactor *= 2.f; - } else - movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); + movementSettings.mSpeedFactor = std::min(vec.length(), 1.f); vec.normalize(); + // TODO: Move this check to mwinput. + // Joystick analogue movement. + // Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used. + if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f) + movementSettings.mSpeedFactor *= 2.f; + + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement && !isFirstPersonPlayer) + { + float angle = mPtr.getRefData().getPosition().rot[2]; + osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor; + osg::Vec2f delta = targetSpeed - mSmoothedSpeed; + float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length(); + float deltaLen = delta.length(); + + float maxDelta; + if (std::abs(speedDelta) < deltaLen / 2) + // Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point). + maxDelta = duration * (isPlayer ? 3.f : 6.f); + else if (isPlayer && speedDelta < -deltaLen / 2) + // As soon as controls are released, mwinput switches player from running to walking. + // So stopping should be instant for player, otherwise it causes a small twitch. + maxDelta = 1; + else // In all other cases speeding up and stopping are smooth. + maxDelta = duration * 3.f; + + if (deltaLen > maxDelta) + delta *= maxDelta / deltaLen; + mSmoothedSpeed += delta; + + osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle); + movementSettings.mSpeedFactor = newSpeed.normalize(); + vec.x() = newSpeed.x(); + vec.y() = newSpeed.y(); + + const float eps = 0.001f; + if (movementSettings.mSpeedFactor < eps) + { + movementSettings.mSpeedFactor = 0; + vec.x() = 0; + vec.y() = 1; + } + else if ((vec.y() < 0) != mIsMovingBackward) + { + if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward) + vec.y() = mIsMovingBackward ? -eps : eps; + } + vec.normalize(); + } + float effectiveRotation = rot.z(); + bool canMove = cls.getMaxSpeed(mPtr) > 0; static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game"); - if (turnToMovementDirection && !isFirstPersonPlayer) + if (!turnToMovementDirection || isFirstPersonPlayer) + movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; + else if (canMove) { float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y()); movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater) @@ -2176,14 +2223,14 @@ void CharacterController::update(float duration, bool animationOnly) stats.setSideMovementAngle(stats.getSideMovementAngle() + delta); effectiveRotation += delta; } - else - movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2; mAnimation->setLegsYawRadians(stats.getSideMovementAngle()); if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater) mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2); else mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4); + if (smoothMovement && !isPlayer && !inwater) + mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2); speed = cls.getCurrentSpeed(mPtr); vec.x() *= speed; @@ -2375,13 +2422,11 @@ void CharacterController::update(float duration, bool animationOnly) : (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack))); } - else if (effectiveRotation != 0.0f) + else { // Do not play turning animation for player if rotation speed is very slow. // Actual threshold should take framerate in account. - float rotationThreshold = 0.f; - if (isPlayer) - rotationThreshold = 0.015 * 60 * duration; + float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration; // It seems only bipedal actors use turning animations. // Also do not use turning animations in the first-person view and when sneaking. @@ -2915,10 +2960,9 @@ void CharacterController::setVisibility(float visibility) void CharacterController::setAttackTypeBasedOnMovement() { float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition; - - if (move[1] && !move[0]) // forward-backward + if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward mAttackType = "thrust"; - else if (move[0] && !move[1]) //sideway + else if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway mAttackType = "slash"; else mAttackType = "chop"; @@ -3126,19 +3170,21 @@ void CharacterController::updateHeadTracking(float duration) return; const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0); - zAngleRadians = std::atan2(direction.x(), direction.y()) - std::atan2(actorDirection.x(), actorDirection.y()); - xAngleRadians = -std::asin(direction.z()); - - const double xLimit = osg::DegreesToRadians(40.0); - const double zLimit = osg::DegreesToRadians(30.0); - zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), -xLimit, xLimit); - xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -zLimit, zLimit); + zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y()); + xAngleRadians = std::asin(direction.z()); } + const double xLimit = osg::DegreesToRadians(40.0); + const double zLimit = osg::DegreesToRadians(30.0); + double zLimitOffset = mAnimation->getUpperBodyYawRadians(); + xAngleRadians = osg::clampBetween(Misc::normalizeAngle(xAngleRadians), -xLimit, xLimit); + zAngleRadians = osg::clampBetween(Misc::normalizeAngle(zAngleRadians), + -zLimit + zLimitOffset, zLimit + zLimitOffset); + float factor = duration*5; factor = std::min(factor, 1.f); - xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * (-xAngleRadians); - zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * (-zAngleRadians); + xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians; + zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians; mAnimation->setHeadPitch(xAngleRadians); mAnimation->setHeadYaw(zAngleRadians); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 9bd11c8f6..4ef4321cc 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -196,6 +196,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener float mTimeUntilWake; bool mIsMovingBackward; + osg::Vec2f mSmoothedSpeed; void setAttackTypeBasedOnMovement(); diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 4b52b3364..a58a2dbad 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -707,7 +707,7 @@ namespace MWMechanics return mTimeOfDeath; } - std::map& CreatureStats::getSummonedCreatureMap() + std::map& CreatureStats::getSummonedCreatureMap() { return mSummonedCreatures; } @@ -725,9 +725,9 @@ namespace MWMechanics */ void CreatureStats::setSummonedCreatureActorId(std::string refId, int actorId) { - for (std::map::iterator it = mSummonedCreatures.begin(); it != mSummonedCreatures.end(); ) + for (std::map::iterator it = mSummonedCreatures.begin(); it != mSummonedCreatures.end(); ) { - if (Misc::StringUtils::ciEqual(getSummonedCreature(it->first.first), refId) && it->second == -1) + if (Misc::StringUtils::ciEqual(getSummonedCreature(it->first.mEffectId), refId) && it->second == -1) { it->second = actorId; break; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 9ac678a61..9b57d5a4e 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -13,6 +13,7 @@ #include "drawstate.hpp" #include +#include namespace ESM { @@ -83,10 +84,8 @@ namespace MWMechanics // The difference between view direction and lower body direction. float mSideMovementAngle; - public: - typedef std::pair SummonKey; // private: - std::map mSummonedCreatures; // + std::map mSummonedCreatures; // // Contains ActorIds of summoned creatures with an expired lifetime that have not been deleted yet. // This may be necessary when the creature is in an inactive cell. @@ -245,7 +244,7 @@ namespace MWMechanics void setBlock(bool value); bool getBlock() const; - std::map& getSummonedCreatureMap(); // + std::map& getSummonedCreatureMap(); // std::vector& getSummonedCreatureGraveyard(); // ActorIds /* diff --git a/apps/openmw/mwmechanics/enchanting.cpp b/apps/openmw/mwmechanics/enchanting.cpp index d9c4faa64..7f1000489 100644 --- a/apps/openmw/mwmechanics/enchanting.cpp +++ b/apps/openmw/mwmechanics/enchanting.cpp @@ -316,7 +316,7 @@ namespace MWMechanics float priceMultipler = MWBase::Environment::get().getWorld()->getStore().get().find ("fEnchantmentValueMult")->mValue.getFloat(); int price = MWBase::Environment::get().getMechanicsManager()->getBarterOffer(mEnchanter, static_cast(getEnchantPoints() * priceMultipler), true); price *= getEnchantItemsCount() * getTypeMultiplier(); - return price; + return std::max(1, price); } int Enchanting::getGemCharge() const diff --git a/apps/openmw/mwmechanics/linkedeffects.cpp b/apps/openmw/mwmechanics/linkedeffects.cpp index 364358433..b0defac7d 100644 --- a/apps/openmw/mwmechanics/linkedeffects.cpp +++ b/apps/openmw/mwmechanics/linkedeffects.cpp @@ -58,6 +58,7 @@ namespace MWMechanics std::vector absorbEffects; ActiveSpells::ActiveEffect absorbEffect = appliedEffect; absorbEffect.mMagnitude *= -1; + absorbEffect.mEffectIndex = appliedEffect.mEffectIndex; absorbEffects.emplace_back(absorbEffect); // Morrowind negates reflected Absorb spells so the original caster won't be harmed. diff --git a/apps/openmw/mwmechanics/magiceffects.hpp b/apps/openmw/mwmechanics/magiceffects.hpp index 86f5a1804..12735a87f 100644 --- a/apps/openmw/mwmechanics/magiceffects.hpp +++ b/apps/openmw/mwmechanics/magiceffects.hpp @@ -74,8 +74,8 @@ namespace MWMechanics { virtual ~EffectSourceVisitor() { } - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1) = 0; }; diff --git a/apps/openmw/mwmechanics/pathfinding.cpp b/apps/openmw/mwmechanics/pathfinding.cpp index 4d4b5be51..a82dcf717 100644 --- a/apps/openmw/mwmechanics/pathfinding.cpp +++ b/apps/openmw/mwmechanics/pathfinding.cpp @@ -88,6 +88,24 @@ namespace const auto halfExtents = world->getHalfExtents(actor); return 2.0 * halfExtents.z(); } + + // Returns true if turn in `p2` is less than 10 degrees and all the 3 points are almost on one line. + bool isAlmostStraight(const osg::Vec3f& p1, const osg::Vec3f& p2, const osg::Vec3f& p3, float pointTolerance) { + osg::Vec3f v1 = p1 - p2; + osg::Vec3f v3 = p3 - p2; + v1.z() = v3.z() = 0; + float dotProduct = v1.x() * v3.x() + v1.y() * v3.y(); + float crossProduct = v1.x() * v3.y() - v1.y() * v3.x(); + + // Check that the angle between v1 and v3 is less or equal than 10 degrees. + static const float cos170 = std::cos(osg::PI / 180 * 170); + bool checkAngle = dotProduct <= cos170 * v1.length() * v3.length(); + + // Check that distance from p2 to the line (p1, p3) is less or equal than `pointTolerance`. + bool checkDist = std::abs(crossProduct) <= pointTolerance * (p3 - p1).length() * 2; + + return checkAngle && checkDist; + } } namespace MWMechanics @@ -286,6 +304,11 @@ namespace MWMechanics while (mPath.size() > 1 && sqrDistanceIgnoreZ(mPath.front(), position) < pointTolerance * pointTolerance) mPath.pop_front(); + while (mPath.size() > 2 && isAlmostStraight(mPath[0], mPath[1], mPath[2], pointTolerance)) + mPath.erase(mPath.begin() + 1); + if (mPath.size() > 1 && isAlmostStraight(position, mPath[0], mPath[1], pointTolerance)) + mPath.pop_front(); + if (mPath.size() == 1 && sqrDistanceIgnoreZ(mPath.front(), position) < destinationTolerance * destinationTolerance) mPath.pop_front(); } diff --git a/apps/openmw/mwmechanics/repair.cpp b/apps/openmw/mwmechanics/repair.cpp index 5025ab729..a6b4e483d 100644 --- a/apps/openmw/mwmechanics/repair.cpp +++ b/apps/openmw/mwmechanics/repair.cpp @@ -40,7 +40,8 @@ void Repair::repair(const MWWorld::Ptr &itemToRepair) // reduce number of uses left int uses = mTool.getClass().getItemHealth(mTool); - mTool.getCellRef().setCharge(uses-1); + uses -= std::min(uses, 1); + mTool.getCellRef().setCharge(uses); MWMechanics::CreatureStats& stats = player.getClass().getCreatureStats(player); diff --git a/apps/openmw/mwmechanics/security.cpp b/apps/openmw/mwmechanics/security.cpp index dae51773c..4f3e405b7 100644 --- a/apps/openmw/mwmechanics/security.cpp +++ b/apps/openmw/mwmechanics/security.cpp @@ -47,6 +47,10 @@ namespace MWMechanics !lock.getClass().hasToolTip(lock)) //If it's unlocked or can not be unlocked back out immediately return; + int uses = lockpick.getClass().getItemHealth(lockpick); + if (uses == 0) + return; + int lockStrength = lock.getCellRef().getLockLevel(); float pickQuality = lockpick.get()->mBase->mData.mQuality; @@ -99,9 +103,7 @@ namespace MWMechanics resultMessage = "#{sLockFail}"; } - int uses = lockpick.getClass().getItemHealth(lockpick); - --uses; - lockpick.getCellRef().setCharge(uses); + lockpick.getCellRef().setCharge(uses-1); if (!uses) lockpick.getContainerStore()->remove(lockpick, 1, mActor); } @@ -109,7 +111,11 @@ namespace MWMechanics void Security::probeTrap(const MWWorld::Ptr &trap, const MWWorld::Ptr &probe, std::string& resultMessage, std::string& resultSound) { - if (trap.getCellRef().getTrap() == "") + if (trap.getCellRef().getTrap().empty()) + return; + + int uses = probe.getClass().getItemHealth(probe); + if (uses == 0) return; float probeQuality = probe.get()->mBase->mData.mQuality; @@ -165,9 +171,7 @@ namespace MWMechanics resultMessage = "#{sTrapFail}"; } - int uses = probe.getClass().getItemHealth(probe); - --uses; - probe.getCellRef().setCharge(uses); + probe.getCellRef().setCharge(uses-1); if (!uses) probe.getContainerStore()->remove(probe, 1, mActor); } diff --git a/apps/openmw/mwmechanics/spellabsorption.cpp b/apps/openmw/mwmechanics/spellabsorption.cpp index f38fd78e2..71e1d0aee 100644 --- a/apps/openmw/mwmechanics/spellabsorption.cpp +++ b/apps/openmw/mwmechanics/spellabsorption.cpp @@ -12,6 +12,7 @@ #include "../mwworld/inventorystore.hpp" #include "creaturestats.hpp" +#include "spellutil.hpp" namespace MWMechanics { @@ -23,8 +24,8 @@ namespace MWMechanics GetAbsorptionProbability() = default; - virtual void visit (MWMechanics::EffectKey key, - const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, + virtual void visit (MWMechanics::EffectKey key, int /*effectIndex*/, + const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float magnitude, float /*remainingTime*/, float /*totalTime*/) { if (key.mId == ESM::MagicEffect::SpellAbsorption) @@ -43,9 +44,9 @@ namespace MWMechanics } }; - bool absorbSpell (const ESM::Spell* spell, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) + bool absorbSpell (const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target) { - if (!spell || caster == target || !target.getClass().isActor()) + if (spellId.empty() || caster == target || !target.getClass().isActor()) return false; CreatureStats& stats = target.getClass().getCreatureStats(target); @@ -62,13 +63,27 @@ namespace MWMechanics if (Misc::Rng::roll0to99() >= chance) return false; - const ESM::Static* absorbStatic = MWBase::Environment::get().getWorld()->getStore().get().find("VFX_Absorb"); + const auto& esmStore = MWBase::Environment::get().getWorld()->getStore(); + const ESM::Static* absorbStatic = esmStore.get().find("VFX_Absorb"); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) animation->addEffect( "meshes\\" + absorbStatic->mModel, ESM::MagicEffect::SpellAbsorption, false, std::string()); + const ESM::Spell* spell = esmStore.get().search(spellId); + int spellCost = 0; + if (spell) + { + spellCost = spell->mData.mCost; + } + else + { + const ESM::Enchantment* enchantment = esmStore.get().search(spellId); + if (enchantment) + spellCost = getEffectiveEnchantmentCastCost(static_cast(enchantment->mData.mCost), caster); + } + // Magicka is increased by the cost of the spell DynamicStat magicka = stats.getMagicka(); - magicka.setCurrent(magicka.getCurrent() + spell->mData.mCost); + magicka.setCurrent(magicka.getCurrent() + spellCost); stats.setMagicka(magicka); return true; } diff --git a/apps/openmw/mwmechanics/spellabsorption.hpp b/apps/openmw/mwmechanics/spellabsorption.hpp index 147090d96..0fe501df9 100644 --- a/apps/openmw/mwmechanics/spellabsorption.hpp +++ b/apps/openmw/mwmechanics/spellabsorption.hpp @@ -1,10 +1,7 @@ #ifndef MWMECHANICS_SPELLABSORPTION_H #define MWMECHANICS_SPELLABSORPTION_H -namespace ESM -{ - struct Spell; -} +#include namespace MWWorld { @@ -14,7 +11,7 @@ namespace MWWorld namespace MWMechanics { // Try to absorb a spell based on the magnitude of every Spell Absorption effect source on the target. - bool absorbSpell(const ESM::Spell* spell, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); + bool absorbSpell(const std::string& spellId, const MWWorld::Ptr& caster, const MWWorld::Ptr& target); } #endif diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 32e5e48c2..fdfb5758f 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -136,10 +136,11 @@ namespace MWMechanics // effects, we display a "can't re-cast" message // Try absorbing the spell. Some handling must still happen for absorbed effects. - bool absorbed = absorbSpell(spell, caster, target); + bool absorbed = absorbSpell(mId, caster, target); + int currentEffectIndex = 0; for (std::vector::const_iterator effectIt (effects.mList.begin()); - !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt) + !target.isEmpty() && effectIt != effects.mList.end(); ++effectIt, ++currentEffectIndex) { if (effectIt->mRange != range) continue; @@ -206,6 +207,7 @@ namespace MWMechanics effect.mArg = MWMechanics::EffectKey(*effectIt).mArg; effect.mMagnitude = magnitude; effect.mTimeLeft = 0.f; + effect.mEffectIndex = currentEffectIndex; // Avoid applying absorb effects if the caster is the target // We still need the spell to be added @@ -309,7 +311,8 @@ namespace MWMechanics if (isSummoningEffect(effectIt->mEffectID) && !target.isEmpty() && target.getClass().isActor()) { CreatureStats& targetStats = target.getClass().getCreatureStats(target); - std::map::iterator findCreature = targetStats.getSummonedCreatureMap().find(std::make_pair(effectIt->mEffectID, mId)); + ESM::SummonKey key(effectIt->mEffectID, mId, currentEffectIndex); + auto findCreature = targetStats.getSummonedCreatureMap().find(key); if (findCreature != targetStats.getSummonedCreatureMap().end()) { /* diff --git a/apps/openmw/mwmechanics/spells.cpp b/apps/openmw/mwmechanics/spells.cpp index a90365e8a..5ce50b544 100644 --- a/apps/openmw/mwmechanics/spells.cpp +++ b/apps/openmw/mwmechanics/spells.cpp @@ -61,7 +61,10 @@ namespace MWMechanics for (const auto& effect : spell->mEffects.mList) { if (iter.second.mPurgedEffects.find(i) != iter.second.mPurgedEffects.end()) + { + ++i; continue; // effect was purged + } float random = 1.f; if (iter.second.mEffectRands.find(i) != iter.second.mEffectRands.end()) @@ -119,7 +122,7 @@ namespace MWMechanics SpellParams params; params.mEffectRands = random; - mSpells.insert (std::make_pair (spell, params)); + mSpells.emplace(spell, params); mSpellsChanged = true; } } @@ -293,7 +296,8 @@ namespace MWMechanics const ESM::Spell * spell = it.first; for (const auto& effectIt : it.second) { - visitor.visit(effectIt.first, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); + // FIXME: since Spells merges effects with the same ID, there is no sense to use multiple effects with same ID here + visitor.visit(effectIt.first, -1, spell->mName, spell->mId, -1, effectIt.second.getMagnitude()); } } } @@ -329,20 +333,24 @@ namespace MWMechanics void Spells::purgeEffect(int effectId, const std::string & sourceId) { - const ESM::Spell * spell = SpellList::getSpell(sourceId); + // Effect source may be not a spell + const ESM::Spell * spell = MWBase::Environment::get().getWorld()->getStore().get().search(sourceId); + if (spell == nullptr) + return; + auto spellIt = mSpells.find(spell); if (spellIt == mSpells.end()) return; - int i = 0; + int index = 0; for (auto& effectIt : spellIt->first->mEffects.mList) { if (effectIt.mEffectID == effectId) { - spellIt->second.mPurgedEffects.insert(i); + spellIt->second.mPurgedEffects.insert(index); mSpellsChanged = true; } - ++i; + ++index; } } @@ -462,7 +470,7 @@ namespace MWMechanics ESM::SpellState::SpellParams params; params.mEffectRands = it.second.mEffectRands; params.mPurgedEffects = it.second.mPurgedEffects; - state.mSpells.insert(std::make_pair(it.first->mId, params)); + state.mSpells.emplace(it.first->mId, params); } } diff --git a/apps/openmw/mwmechanics/steering.cpp b/apps/openmw/mwmechanics/steering.cpp index d442085ea..eaf37fbd2 100644 --- a/apps/openmw/mwmechanics/steering.cpp +++ b/apps/openmw/mwmechanics/steering.cpp @@ -1,5 +1,8 @@ #include "steering.hpp" +#include +#include + #include "../mwworld/class.hpp" #include "../mwworld/ptr.hpp" @@ -12,19 +15,8 @@ namespace MWMechanics bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, float epsilonRadians) { - float currentAngle (actor.getRefData().getPosition().rot[axis]); - float diff (targetAngleRadians - currentAngle); - if (std::abs(diff) >= osg::DegreesToRadians(180.f)) - { - if (diff >= 0) - { - diff = diff - osg::DegreesToRadians(360.f); - } - else - { - diff = osg::DegreesToRadians(360.f) + diff; - } - } + MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); + float diff = Misc::normalizeAngle(targetAngleRadians - actor.getRefData().getPosition().rot[axis]); float absDiff = std::abs(diff); // The turning animation actually moves you slightly, so the angle will be wrong again. @@ -33,10 +25,14 @@ bool smoothTurn(const MWWorld::Ptr& actor, float targetAngleRadians, int axis, f return true; float limit = getAngularVelocity(actor.getClass().getMaxSpeed(actor)) * MWBase::Environment::get().getFrameDuration(); + static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game"); + if (smoothMovement) + limit *= std::min(absDiff / osg::PI + 0.1, 0.5); + if (absDiff > limit) diff = osg::sign(diff) * limit; - actor.getClass().getMovementSettings(actor).mRotation[axis] = diff; + movement.mRotation[axis] = diff; return false; } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 8d28075a6..6d228dabe 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -80,25 +80,25 @@ namespace MWMechanics { } - void UpdateSummonedCreatures::visit(EffectKey key, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) + void UpdateSummonedCreatures::visit(EffectKey key, int effectIndex, const std::string &sourceName, const std::string &sourceId, int casterActorId, float magnitude, float remainingTime, float totalTime) { if (isSummoningEffect(key.mId) && magnitude > 0) { - mActiveEffects.insert(std::make_pair(key.mId, sourceId)); + mActiveEffects.insert(ESM::SummonKey(key.mId, sourceId, effectIndex)); } } void UpdateSummonedCreatures::process(bool cleanup) { MWMechanics::CreatureStats& creatureStats = mActor.getClass().getCreatureStats(mActor); - std::map& creatureMap = creatureStats.getSummonedCreatureMap(); + std::map& creatureMap = creatureStats.getSummonedCreatureMap(); - for (std::set >::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) + for (std::set::iterator it = mActiveEffects.begin(); it != mActiveEffects.end(); ++it) { - bool found = creatureMap.find(std::make_pair(it->first, it->second)) != creatureMap.end(); + bool found = creatureMap.find(*it) != creatureMap.end(); if (!found) { - std::string creatureID = getSummonedCreature(it->first); + std::string creatureID = getSummonedCreature(it->mEffectId); if (!creatureID.empty()) { int creatureActorId = -1; @@ -146,10 +146,10 @@ namespace MWMechanics objectList->packetOrigin = mwmp::CLIENT_GAMEPLAY; MWMechanics::CreatureStats *actorCreatureStats = &mActor.getClass().getCreatureStats(mActor); - int effectId = it->first; - std::string spellId = it->second; - float duration = actorCreatureStats->getActiveSpells().getEffectDuration(effectId, it->second); - objectList->addObjectSpawn(placed, mActor, spellId, effectId, duration); + int effectId = it->mEffectId; + std::string sourceId = it->mSourceId; + float duration = actorCreatureStats->getActiveSpells().getEffectDuration(effectId, sourceId); + objectList->addObjectSpawn(placed, mActor, sourceId, effectId, duration); objectList->sendObjectSpawn(); } @@ -161,7 +161,7 @@ namespace MWMechanics // still insert into creatureMap so we don't try to spawn again every frame, that would spam the warning log } - creatureMap.insert(std::make_pair(*it, creatureActorId)); + creatureMap.emplace(*it, creatureActorId); /* End of tes3mp change (major) */ @@ -170,7 +170,7 @@ namespace MWMechanics } // Update summon effects - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) { bool found = mActiveEffects.find(it->first) != mActiveEffects.end(); if (!found) @@ -192,7 +192,7 @@ namespace MWMechanics if (!cleanup) return; - for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) { /* Start of tes3mp addition @@ -214,9 +214,11 @@ namespace MWMechanics if (ptr.isEmpty() || (ptr.getClass().getCreatureStats(ptr).isDead() && ptr.getClass().getCreatureStats(ptr).isDeathAnimationFinished())) { // Purge the magic effect so a new creature can be summoned if desired - creatureStats.getActiveSpells().purgeEffect(it->first.first, it->first.second); + const ESM::SummonKey& key = it->first; + creatureStats.getActiveSpells().purgeEffect(key.mEffectId, key.mSourceId, key.mEffectIndex); + creatureStats.getSpells().purgeEffect(key.mEffectId, key.mSourceId); if (mActor.getClass().hasInventoryStore(mActor)) - mActor.getClass().getInventoryStore(mActor).purgeEffect(it->first.first, it->first.second); + mActor.getClass().getInventoryStore(mActor).purgeEffect(key.mEffectId, key.mSourceId, false, key.mEffectIndex); MWBase::Environment::get().getMechanicsManager()->cleanupSummonedCreature(mActor, it->second); creatureMap.erase(it++); diff --git a/apps/openmw/mwmechanics/summoning.hpp b/apps/openmw/mwmechanics/summoning.hpp index f24413120..ac820e32f 100644 --- a/apps/openmw/mwmechanics/summoning.hpp +++ b/apps/openmw/mwmechanics/summoning.hpp @@ -5,6 +5,8 @@ #include "../mwworld/ptr.hpp" +#include + #include "magiceffects.hpp" namespace MWMechanics @@ -20,8 +22,8 @@ namespace MWMechanics UpdateSummonedCreatures(const MWWorld::Ptr& actor); virtual ~UpdateSummonedCreatures() = default; - virtual void visit (MWMechanics::EffectKey key, - const std::string& sourceName, const std::string& sourceId, int casterActorId, + virtual void visit (MWMechanics::EffectKey key, int effectIndex, + const std::string& sourceName, const std::string& sourceId, int casterActorId, float magnitude, float remainingTime = -1, float totalTime = -1); /// To call after all effect sources have been visited @@ -30,7 +32,7 @@ namespace MWMechanics private: MWWorld::Ptr mActor; - std::set > mActiveEffects; + std::set mActiveEffects; }; } diff --git a/apps/openmw/mwmp/ObjectList.cpp b/apps/openmw/mwmp/ObjectList.cpp index 778730a0f..e14f31697 100644 --- a/apps/openmw/mwmp/ObjectList.cpp +++ b/apps/openmw/mwmp/ObjectList.cpp @@ -508,15 +508,30 @@ void ObjectList::spawnObjects(MWWorld::CellStore* cellStore) newPtr.getCellRef().getRefNum(), newPtr.getCellRef().getMpNum(), creatureActorId); // Check if this creature is present in the summoner's summoned creature map - std::map, int>& creatureMap = masterCreatureStats.getSummonedCreatureMap(); - bool foundSummonedCreature = creatureMap.find(std::make_pair(baseObject.summonEffectId, baseObject.summonSpellId)) != creatureMap.end(); + std::map& creatureMap = masterCreatureStats.getSummonedCreatureMap(); + + bool foundSummonedCreature = false; + + for (std::map::iterator it = creatureMap.begin(); it != creatureMap.end(); ) + { + if (it->first.mEffectId == baseObject.summonEffectId && it->first.mSourceId == baseObject.summonSpellId) + { + foundSummonedCreature = true; + break; + } + } // If it is, update its creatureActorId if (foundSummonedCreature) + { masterCreatureStats.setSummonedCreatureActorId(baseObject.refId, creatureActorId); + } // If not, add it to the summoned creature map else - creatureMap.insert(std::make_pair(std::make_pair(baseObject.summonEffectId, baseObject.summonSpellId), creatureActorId)); + { + ESM::SummonKey summonKey(baseObject.summonEffectId, baseObject.summonSpellId, -1); + creatureMap.emplace(summonKey, creatureActorId); + } creatureStats.setFriendlyHits(0); } diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index a04a3f999..8a1719db4 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -474,6 +474,7 @@ public: void setAlpha(float alpha); virtual void setPitchFactor(float factor) {} virtual void attachArrow() {} + virtual void detachArrow() {} virtual void releaseArrow(float attackStrength) {} virtual void enableHeadAnimation(bool enable) {} // TODO: move outside of this class diff --git a/apps/openmw/mwrender/creatureanimation.cpp b/apps/openmw/mwrender/creatureanimation.cpp index 489a7a987..4a832c60c 100644 --- a/apps/openmw/mwrender/creatureanimation.cpp +++ b/apps/openmw/mwrender/creatureanimation.cpp @@ -208,6 +208,12 @@ bool CreatureWeaponAnimation::isArrowAttached() const return mAmmunition != nullptr; } +void CreatureWeaponAnimation::detachArrow() +{ + WeaponAnimation::detachArrow(mPtr); + updateQuiver(); +} + void CreatureWeaponAnimation::attachArrow() { WeaponAnimation::attachArrow(mPtr); diff --git a/apps/openmw/mwrender/creatureanimation.hpp b/apps/openmw/mwrender/creatureanimation.hpp index cdcdafe24..071500d74 100644 --- a/apps/openmw/mwrender/creatureanimation.hpp +++ b/apps/openmw/mwrender/creatureanimation.hpp @@ -40,6 +40,7 @@ namespace MWRender void updatePart(PartHolderPtr& scene, int slot); virtual void attachArrow(); + virtual void detachArrow(); virtual void releaseArrow(float attackStrength); // WeaponAnimation virtual osg::Group* getArrowBone(); diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 468938d22..31cf3f015 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -363,6 +363,7 @@ void NpcAnimation::setViewMode(NpcAnimation::ViewMode viewMode) mViewMode = viewMode; MWBase::Environment::get().getWorld()->scaleObject(mPtr, mPtr.getCellRef().getScale()); // apply race height after view change + mAmmunition.reset(); rebuild(); setRenderBin(); } @@ -1051,6 +1052,12 @@ void NpcAnimation::attachArrow() updateQuiver(); } +void NpcAnimation::detachArrow() +{ + WeaponAnimation::detachArrow(mPtr); + updateQuiver(); +} + void NpcAnimation::releaseArrow(float attackStrength) { WeaponAnimation::releaseArrow(mPtr, attackStrength); diff --git a/apps/openmw/mwrender/npcanimation.hpp b/apps/openmw/mwrender/npcanimation.hpp index 7edf35a5c..cf695c878 100644 --- a/apps/openmw/mwrender/npcanimation.hpp +++ b/apps/openmw/mwrender/npcanimation.hpp @@ -142,6 +142,7 @@ public: virtual void showCarriedLeft(bool show); virtual void attachArrow(); + virtual void detachArrow(); virtual void releaseArrow(float attackStrength); virtual osg::Group* getArrowBone(); diff --git a/apps/openmw/mwrender/objectpaging.cpp b/apps/openmw/mwrender/objectpaging.cpp index 0f7e1c422..c756a3fc7 100644 --- a/apps/openmw/mwrender/objectpaging.cpp +++ b/apps/openmw/mwrender/objectpaging.cpp @@ -289,7 +289,9 @@ namespace MWRender } virtual void apply(osg::Geometry& geom) { - mResult.mNumVerts += geom.getVertexArray()->getNumElements(); + if (osg::Array* array = geom.getVertexArray()) + mResult.mNumVerts += array->getNumElements(); + ++mResult.mStateSetCounter[mCurrentStateSet]; ++mGlobalStateSetCounter[mCurrentStateSet]; } diff --git a/apps/openmw/mwrender/terrainstorage.hpp b/apps/openmw/mwrender/terrainstorage.hpp index 14bed7b7b..c9ad94398 100644 --- a/apps/openmw/mwrender/terrainstorage.hpp +++ b/apps/openmw/mwrender/terrainstorage.hpp @@ -17,7 +17,7 @@ namespace MWRender { public: - TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern = "", const std::string& normalHeightMapPatteern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false); + TerrainStorage(Resource::ResourceSystem* resourceSystem, const std::string& normalMapPattern = "", const std::string& normalHeightMapPattern = "", bool autoUseNormalMaps = false, const std::string& specularMapPattern = "", bool autoUseSpecularMaps = false); ~TerrainStorage(); virtual osg::ref_ptr getLand (int cellX, int cellY) override; diff --git a/apps/openmw/mwrender/water.cpp b/apps/openmw/mwrender/water.cpp index c9d16b728..a1691d5d1 100644 --- a/apps/openmw/mwrender/water.cpp +++ b/apps/openmw/mwrender/water.cpp @@ -443,6 +443,7 @@ Water::Water(osg::Group *parent, osg::Group* sceneRoot, Resource::ResourceSystem mWaterGeom = SceneUtil::createWaterGeometry(Constants::CellSizeInUnits*150, 40, 900); mWaterGeom->setDrawCallback(new DepthClampCallback); mWaterGeom->setNodeMask(Mask_Water); + mWaterGeom->setDataVariance(osg::Object::STATIC); mWaterNode = new osg::PositionAttitudeTransform; mWaterNode->setName("Water Root"); diff --git a/apps/openmw/mwrender/weaponanimation.cpp b/apps/openmw/mwrender/weaponanimation.cpp index 0ba5baec6..11eb60f88 100644 --- a/apps/openmw/mwrender/weaponanimation.cpp +++ b/apps/openmw/mwrender/weaponanimation.cpp @@ -108,6 +108,11 @@ void WeaponAnimation::attachArrow(MWWorld::Ptr actor) } } +void WeaponAnimation::detachArrow(MWWorld::Ptr actor) +{ + mAmmunition.reset(); +} + void WeaponAnimation::releaseArrow(MWWorld::Ptr actor, float attackStrength) { MWWorld::InventoryStore& inv = actor.getClass().getInventoryStore(actor); diff --git a/apps/openmw/mwrender/weaponanimation.hpp b/apps/openmw/mwrender/weaponanimation.hpp index a1988703c..dac1b663d 100644 --- a/apps/openmw/mwrender/weaponanimation.hpp +++ b/apps/openmw/mwrender/weaponanimation.hpp @@ -36,6 +36,8 @@ namespace MWRender /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. void attachArrow(MWWorld::Ptr actor); + void detachArrow(MWWorld::Ptr actor); + /// @note If no weapon (or an invalid weapon) is equipped, this function is a no-op. void releaseArrow(MWWorld::Ptr actor, float attackStrength); diff --git a/apps/openmw/mwscript/transformationextensions.cpp b/apps/openmw/mwscript/transformationextensions.cpp index ec7a066a1..4c8b1d7ad 100644 --- a/apps/openmw/mwscript/transformationextensions.cpp +++ b/apps/openmw/mwscript/transformationextensions.cpp @@ -219,12 +219,20 @@ namespace MWScript float ay = ptr.getRefData().getPosition().rot[1]; float az = ptr.getRefData().getPosition().rot[2]; + // XYZ axis use the inverse (XYZ) rotation order like vanilla SetAngle. + // UWV axis use the standard (ZYX) rotation order like TESCS/OpenMW-CS and the rest of the game. if (axis == "x") - MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az); + MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_inverseOrder); else if (axis == "y") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az); + MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_inverseOrder); else if (axis == "z") - MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle); + MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_inverseOrder); + else if (axis == "u") + MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az,MWBase::RotationFlag_none); + else if (axis == "w") + MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az,MWBase::RotationFlag_none); + else if (axis == "v") + MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle,MWBase::RotationFlag_none); } }; diff --git a/apps/openmw/mwworld/inventorystore.cpp b/apps/openmw/mwworld/inventorystore.cpp index fa4b9ac40..659bacafa 100644 --- a/apps/openmw/mwworld/inventorystore.cpp +++ b/apps/openmw/mwworld/inventorystore.cpp @@ -978,7 +978,7 @@ void MWWorld::InventoryStore::visitEffectSources(MWMechanics::EffectSourceVisito float magnitude = effect.mMagnMin + (effect.mMagnMax - effect.mMagnMin) * params.mRandom; magnitude *= params.mMultiplier; if (magnitude > 0) - visitor.visit(MWMechanics::EffectKey(effect), (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); + visitor.visit(MWMechanics::EffectKey(effect), i-1, (**iter).getClass().getName(**iter), (**iter).getCellRef().getRefId(), -1, magnitude); } } } @@ -992,7 +992,7 @@ void MWWorld::InventoryStore::purgeEffect(short effectId, bool wholeSpell) } } -void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell) +void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sourceId, bool wholeSpell, int effectIndex) { TEffectMagnitudes::iterator effectMagnitudeIt = mPermanentMagicEffectMagnitudes.find(sourceId); if (effectMagnitudeIt == mPermanentMagicEffectMagnitudes.end()) @@ -1025,6 +1025,9 @@ void MWWorld::InventoryStore::purgeEffect(short effectId, const std::string &sou if (effectIt->mEffectID != effectId) continue; + if (effectIndex >= 0 && effectIndex != i) + continue; + if (wholeSpell) { mPermanentMagicEffectMagnitudes.erase(sourceId); diff --git a/apps/openmw/mwworld/inventorystore.hpp b/apps/openmw/mwworld/inventorystore.hpp index d597e5f30..97ca931e7 100644 --- a/apps/openmw/mwworld/inventorystore.hpp +++ b/apps/openmw/mwworld/inventorystore.hpp @@ -206,7 +206,7 @@ namespace MWWorld void purgeEffect (short effectId, bool wholeSpell = false); ///< Remove a magic effect - void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false); + void purgeEffect (short effectId, const std::string& sourceId, bool wholeSpell = false, int effectIndex=-1); ///< Remove a magic effect virtual void clear(); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 48cd3faac..c75f7aa6a 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3573,7 +3573,7 @@ namespace MWWorld { } - virtual void visit (MWMechanics::EffectKey key, + virtual void visit (MWMechanics::EffectKey key, int /*effectIndex*/, const std::string& /*sourceName*/, const std::string& /*sourceId*/, int /*casterActorId*/, float /*magnitude*/, float /*remainingTime*/ = -1, float /*totalTime*/ = -1) { diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index e1f87a8fe..cc3530576 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -308,9 +308,9 @@ namespace MWWorld ///< Return a pointer to a liveCellRef with the given name. /// \param activeOnly do non search inactive cells. - Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = true) override; + Ptr searchPtr (const std::string& name, bool activeOnly, bool searchInContainers = false) override; ///< Return a pointer to a liveCellRef with the given name. - /// \param activeOnly do non search inactive cells. + /// \param activeOnly do not search inactive cells. Ptr searchPtrViaActorId (int actorId) override; ///< Search is limited to the active cells. diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 0ca116f2e..63df7e413 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -91,10 +91,9 @@ add_component_dir (misc gcd constants utf8stream stringops resourcehelpers rng messageformatparser weakcache ) -add_component_dir(debug - debugging debuglog - ) - +add_component_dir (debug + debugging debuglog gldebug + ) IF(NOT WIN32 AND NOT APPLE) add_definitions(-DGLOBAL_DATA_PATH="${GLOBAL_DATA_PATH}") diff --git a/components/debug/gldebug.cpp b/components/debug/gldebug.cpp new file mode 100644 index 000000000..3c5ec728a --- /dev/null +++ b/components/debug/gldebug.cpp @@ -0,0 +1,164 @@ +// This file is based heavily on code from https://github.com/ThermalPixel/osgdemos/blob/master/osgdebug/EnableGLDebugOperation.cpp +// The original licence is included below: +/* +Copyright (c) 2014, Andreas Klein +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. +*/ + +#include "gldebug.hpp" + +#include + +#include + +// OpenGL constants not provided by OSG: +#include + +void debugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) +{ +#ifdef GL_DEBUG_OUTPUT + std::string srcStr; + switch (source) + { + case GL_DEBUG_SOURCE_API: + srcStr = "API"; + break; + case GL_DEBUG_SOURCE_WINDOW_SYSTEM: + srcStr = "WINDOW_SYSTEM"; + break; + case GL_DEBUG_SOURCE_SHADER_COMPILER: + srcStr = "SHADER_COMPILER"; + break; + case GL_DEBUG_SOURCE_THIRD_PARTY: + srcStr = "THIRD_PARTY"; + break; + case GL_DEBUG_SOURCE_APPLICATION: + srcStr = "APPLICATION"; + break; + case GL_DEBUG_SOURCE_OTHER: + srcStr = "OTHER"; + break; + default: + srcStr = "UNDEFINED"; + break; + } + + std::string typeStr; + + Debug::Level logSeverity = Debug::Warning; + switch (type) + { + case GL_DEBUG_TYPE_ERROR: + typeStr = "ERROR"; + logSeverity = Debug::Error; + break; + case GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR: + typeStr = "DEPRECATED_BEHAVIOR"; + break; + case GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR: + typeStr = "UNDEFINED_BEHAVIOR"; + break; + case GL_DEBUG_TYPE_PORTABILITY: + typeStr = "PORTABILITY"; + break; + case GL_DEBUG_TYPE_PERFORMANCE: + typeStr = "PERFORMANCE"; + break; + case GL_DEBUG_TYPE_OTHER: + typeStr = "OTHER"; + break; + default: + typeStr = "UNDEFINED"; + break; + } + + Log(logSeverity) << "OpenGL " << typeStr << " [" << srcStr << "]: " << message; +#endif +} + +void enableGLDebugExtension(unsigned int contextID) +{ +#ifdef GL_DEBUG_OUTPUT + typedef void (GL_APIENTRY *DEBUGPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam); + typedef void (GL_APIENTRY *GLDebugMessageControlFunction)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint *ids, GLboolean enabled); + typedef void (GL_APIENTRY *GLDebugMessageCallbackFunction)(DEBUGPROC, const void* userParam); + + GLDebugMessageControlFunction glDebugMessageControl = nullptr; + GLDebugMessageCallbackFunction glDebugMessageCallback = nullptr; + + if (osg::isGLExtensionSupported(contextID, "GL_KHR_debug")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallback"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControl"); + } + else if (osg::isGLExtensionSupported(contextID, "GL_ARB_debug_output")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackARB"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlARB"); + } + else if (osg::isGLExtensionSupported(contextID, "GL_AMD_debug_output")) + { + osg::setGLExtensionFuncPtr(glDebugMessageCallback, "glDebugMessageCallbackAMD"); + osg::setGLExtensionFuncPtr(glDebugMessageControl, "glDebugMessageControlAMD"); + } + + if (glDebugMessageCallback && glDebugMessageControl) + { + glEnable(GL_DEBUG_OUTPUT); + glDebugMessageControl(GL_DONT_CARE, GL_DEBUG_TYPE_ERROR, GL_DEBUG_SEVERITY_MEDIUM, 0, nullptr, true); + glDebugMessageCallback(debugCallback, nullptr); + + Log(Debug::Info) << "OpenGL debug callback attached."; + } + else +#endif + Log(Debug::Error) << "Unable to attach OpenGL debug callback."; +} + +Debug::EnableGLDebugOperation::EnableGLDebugOperation() : osg::GraphicsOperation("EnableGLDebugOperation", false) +{ +} + +void Debug::EnableGLDebugOperation::operator()(osg::GraphicsContext* graphicsContext) +{ + OpenThreads::ScopedLock lock(mMutex); + + unsigned int contextID = graphicsContext->getState()->getContextID(); + enableGLDebugExtension(contextID); +} + +bool Debug::shouldDebugOpenGL() +{ + const char* env = std::getenv("OPENMW_DEBUG_OPENGL"); + if (!env) + return false; + std::string str(env); + if (str.length() == 0) + return true; + + return str.find("OFF") == std::string::npos && str.find("0") == std::string::npos && str.find("NO") == std::string::npos; +} diff --git a/components/debug/gldebug.hpp b/components/debug/gldebug.hpp new file mode 100644 index 000000000..823d4f36f --- /dev/null +++ b/components/debug/gldebug.hpp @@ -0,0 +1,21 @@ +#ifndef OPENMW_COMPONENTS_DEBUG_GLDEBUG_H +#define OPENMW_COMPONENTS_DEBUG_GLDEBUG_H + +#include + +namespace Debug +{ + class EnableGLDebugOperation : public osg::GraphicsOperation + { + public: + EnableGLDebugOperation(); + + virtual void operator()(osg::GraphicsContext* graphicsContext); + + private: + OpenThreads::Mutex mMutex; + }; + + bool shouldDebugOpenGL(); +} +#endif diff --git a/components/esm/activespells.cpp b/components/esm/activespells.cpp index 46558ceb7..4017a4933 100644 --- a/components/esm/activespells.cpp +++ b/components/esm/activespells.cpp @@ -24,6 +24,7 @@ namespace ESM esm.writeHNT ("ARG_", effectIt->mArg); esm.writeHNT ("MAGN", effectIt->mMagnitude); esm.writeHNT ("DURA", effectIt->mDuration); + esm.writeHNT ("EIND", effectIt->mEffectIndex); esm.writeHNT ("LEFT", effectIt->mTimeLeft); } } @@ -53,6 +54,8 @@ namespace ESM esm.getHNOT(effect.mArg, "ARG_"); esm.getHNT (effect.mMagnitude, "MAGN"); esm.getHNT (effect.mDuration, "DURA"); + effect.mEffectIndex = -1; + esm.getHNOT (effect.mEffectIndex, "EIND"); if (format < 9) effect.mTimeLeft = effect.mDuration; else diff --git a/components/esm/activespells.hpp b/components/esm/activespells.hpp index 20b2f652d..1b7f8b319 100644 --- a/components/esm/activespells.hpp +++ b/components/esm/activespells.hpp @@ -22,6 +22,7 @@ namespace ESM int mArg; // skill or attribute float mDuration; float mTimeLeft; + int mEffectIndex; }; // format 0, saved games only diff --git a/components/esm/creaturestats.cpp b/components/esm/creaturestats.cpp index 6f0b36f8d..cb383992c 100644 --- a/components/esm/creaturestats.cpp +++ b/components/esm/creaturestats.cpp @@ -115,9 +115,11 @@ void ESM::CreatureStats::load (ESMReader &esm) int magicEffect; esm.getHT(magicEffect); std::string source = esm.getHNOString("SOUR"); + int effectIndex = -1; + esm.getHNOT (effectIndex, "EIND"); int actorId; esm.getHNT (actorId, "ACID"); - mSummonedCreatureMap[std::make_pair(magicEffect, source)] = actorId; + mSummonedCreatureMap[SummonKey(magicEffect, source, effectIndex)] = actorId; } while (esm.isNextSub("GRAV")) @@ -212,16 +214,19 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSequence.save(esm); mMagicEffects.save(esm); - for (std::map, int>::const_iterator it = mSummonedCreatureMap.begin(); it != mSummonedCreatureMap.end(); ++it) + for (const auto& summon : mSummonedCreatureMap) { - esm.writeHNT ("SUMM", it->first.first); - esm.writeHNString ("SOUR", it->first.second); - esm.writeHNT ("ACID", it->second); + esm.writeHNT ("SUMM", summon.first.mEffectId); + esm.writeHNString ("SOUR", summon.first.mSourceId); + int effectIndex = summon.first.mEffectIndex; + if (effectIndex != -1) + esm.writeHNT ("EIND", effectIndex); + esm.writeHNT ("ACID", summon.second); } - for (std::vector::const_iterator it = mSummonGraveyard.begin(); it != mSummonGraveyard.end(); ++it) + for (int key : mSummonGraveyard) { - esm.writeHNT ("GRAV", *it); + esm.writeHNT ("GRAV", key); } esm.writeHNT("AISE", mHasAiSettings); @@ -231,11 +236,11 @@ void ESM::CreatureStats::save (ESMWriter &esm) const mAiSettings[i].save(esm); } - for (std::map::const_iterator it = mCorprusSpells.begin(); it != mCorprusSpells.end(); ++it) + for (const auto& corprusSpell : mCorprusSpells) { - esm.writeHNString("CORP", it->first); + esm.writeHNString("CORP", corprusSpell.first); - const CorprusStats & stats = it->second; + const CorprusStats & stats = corprusSpell.second; esm.writeHNT("WORS", stats.mWorsenings); esm.writeHNT("TIME", stats.mNextWorsening); } diff --git a/components/esm/creaturestats.hpp b/components/esm/creaturestats.hpp index 79a576587..13bc50008 100644 --- a/components/esm/creaturestats.hpp +++ b/components/esm/creaturestats.hpp @@ -39,7 +39,7 @@ namespace ESM bool mHasAiSettings; StatState mAiSettings[4]; - std::map, int> mSummonedCreatureMap; + std::map mSummonedCreatureMap; std::vector mSummonGraveyard; ESM::TimeStamp mTradeTime; diff --git a/components/esm/magiceffects.hpp b/components/esm/magiceffects.hpp index 2a6052caa..a931c68fa 100644 --- a/components/esm/magiceffects.hpp +++ b/components/esm/magiceffects.hpp @@ -2,6 +2,7 @@ #define COMPONENTS_ESM_MAGICEFFECTS_H #include +#include namespace ESM { @@ -18,6 +19,41 @@ namespace ESM void save (ESMWriter &esm) const; }; + struct SummonKey + { + SummonKey(int effectId, const std::string& sourceId, int index) + { + mEffectId = effectId; + mSourceId = sourceId; + mEffectIndex = index; + } + + bool operator==(const SummonKey &other) const + { + return mEffectId == other.mEffectId && + mSourceId == other.mSourceId && + mEffectIndex == other.mEffectIndex; + } + + bool operator<(const SummonKey &other) const + { + if (mEffectId < other.mEffectId) + return true; + if (mEffectId > other.mEffectId) + return false; + + if (mSourceId < other.mSourceId) + return true; + if (mSourceId > other.mSourceId) + return false; + + return mEffectIndex < other.mEffectIndex; + } + + int mEffectId; + std::string mSourceId; + int mEffectIndex; + }; } #endif diff --git a/components/esm/savedgame.cpp b/components/esm/savedgame.cpp index 4b0529703..0fc84e309 100644 --- a/components/esm/savedgame.cpp +++ b/components/esm/savedgame.cpp @@ -4,7 +4,7 @@ #include "esmwriter.hpp" unsigned int ESM::SavedGame::sRecordId = ESM::REC_SAVE; -int ESM::SavedGame::sCurrentFormat = 13; +int ESM::SavedGame::sCurrentFormat = 14; void ESM::SavedGame::load (ESMReader &esm) { diff --git a/components/nifosg/controller.cpp b/components/nifosg/controller.cpp index 58f1c17a7..b6610728a 100644 --- a/components/nifosg/controller.cpp +++ b/components/nifosg/controller.cpp @@ -449,7 +449,7 @@ void MaterialColorController::apply(osg::StateSet *stateset, osg::NodeVisitor *n } FlipController::FlipController(const Nif::NiFlipController *ctrl, const std::vector >& textures) - : mTexSlot(ctrl->mTexSlot) + : mTexSlot(0) // always affects diffuse , mDelta(ctrl->mDelta) , mTextures(textures) { diff --git a/components/nifosg/nifloader.cpp b/components/nifosg/nifloader.cpp index 21ae49975..f9274cebf 100644 --- a/components/nifosg/nifloader.cpp +++ b/components/nifosg/nifloader.cpp @@ -799,22 +799,23 @@ namespace NifOsg { const Nif::NiFlipController* flipctrl = static_cast(ctrl.getPtr()); std::vector > textures; + + // inherit wrap settings from the target slot + osg::Texture2D* inherit = dynamic_cast(stateset->getTextureAttribute(0, osg::StateAttribute::TEXTURE)); + osg::Texture2D::WrapMode wrapS = osg::Texture2D::REPEAT; + osg::Texture2D::WrapMode wrapT = osg::Texture2D::REPEAT; + if (inherit) + { + wrapS = inherit->getWrap(osg::Texture2D::WRAP_S); + wrapT = inherit->getWrap(osg::Texture2D::WRAP_T); + } + for (unsigned int i=0; imSources.length(); ++i) { Nif::NiSourceTexturePtr st = flipctrl->mSources[i]; if (st.empty()) continue; - // inherit wrap settings from the target slot - osg::Texture2D* inherit = dynamic_cast(stateset->getTextureAttribute(flipctrl->mTexSlot, osg::StateAttribute::TEXTURE)); - osg::Texture2D::WrapMode wrapS = osg::Texture2D::CLAMP_TO_EDGE; - osg::Texture2D::WrapMode wrapT = osg::Texture2D::CLAMP_TO_EDGE; - if (inherit) - { - wrapS = inherit->getWrap(osg::Texture2D::WRAP_S); - wrapT = inherit->getWrap(osg::Texture2D::WRAP_T); - } - osg::ref_ptr image (handleSourceTexture(st.getPtr(), imageManager)); osg::ref_ptr texture (new osg::Texture2D(image)); if (image) @@ -1451,7 +1452,7 @@ namespace NifOsg // If this loop is changed such that the base texture isn't guaranteed to end up in texture unit 0, the shadow casting shader will need to be updated accordingly. for (size_t i=0; itextures.size(); ++i) { - if (texprop->textures[i].inUse) + if (texprop->textures[i].inUse || (i == Nif::NiTexturingProperty::BaseTexture && !texprop->controller.empty())) { switch(i) { @@ -1477,32 +1478,46 @@ namespace NifOsg } } - const Nif::NiTexturingProperty::Texture& tex = texprop->textures[i]; - if(tex.texture.empty() && texprop->controller.empty()) - { - if (i == 0) - Log(Debug::Warning) << "Base texture is in use but empty on shape \"" << nodeName << "\" in " << mFilename; - continue; - } - + unsigned int uvSet = 0; // create a new texture, will later attempt to share using the SharedStateManager osg::ref_ptr texture2d; - if (!tex.texture.empty()) + if (texprop->textures[i].inUse) { - const Nif::NiSourceTexture *st = tex.texture.getPtr(); - osg::ref_ptr image = handleSourceTexture(st, imageManager); - texture2d = new osg::Texture2D(image); - if (image) - texture2d->setTextureSize(image->s(), image->t()); + const Nif::NiTexturingProperty::Texture& tex = texprop->textures[i]; + if(tex.texture.empty() && texprop->controller.empty()) + { + if (i == 0) + Log(Debug::Warning) << "Base texture is in use but empty on shape \"" << nodeName << "\" in " << mFilename; + continue; + } + + if (!tex.texture.empty()) + { + const Nif::NiSourceTexture *st = tex.texture.getPtr(); + osg::ref_ptr image = handleSourceTexture(st, imageManager); + texture2d = new osg::Texture2D(image); + if (image) + texture2d->setTextureSize(image->s(), image->t()); + } + else + texture2d = new osg::Texture2D; + + bool wrapT = tex.clamp & 0x1; + bool wrapS = (tex.clamp >> 1) & 0x1; + + texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + + uvSet = tex.uvSet; } else + { + // Texture only comes from NiFlipController, so tex is ignored, set defaults texture2d = new osg::Texture2D; - - bool wrapT = tex.clamp & 0x1; - bool wrapS = (tex.clamp >> 1) & 0x1; - - texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); - texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE); + texture2d->setWrap(osg::Texture::WRAP_S, osg::Texture::REPEAT); + texture2d->setWrap(osg::Texture::WRAP_T, osg::Texture::REPEAT); + uvSet = 0; + } unsigned int texUnit = boundTextures.size(); @@ -1590,7 +1605,7 @@ namespace NifOsg break; } - boundTextures.push_back(tex.uvSet); + boundTextures.push_back(uvSet); } } handleTextureControllers(texprop, composite, imageManager, stateset, animflags); diff --git a/components/resource/imagemanager.cpp b/components/resource/imagemanager.cpp index 41ce999e0..ff6fb04a6 100644 --- a/components/resource/imagemanager.cpp +++ b/components/resource/imagemanager.cpp @@ -115,6 +115,29 @@ namespace Resource return mWarningImage; } + bool killAlpha = false; + if (reader->supportedExtensions().count("tga")) + { + // Morrowind ignores the alpha channel of 16bpp TGA files even when the header says not to + unsigned char header[18]; + stream->read((char*)header, 18); + if (stream->gcount() != 18) + { + Log(Debug::Error) << "Error loading " << filename << ": couldn't read TGA header"; + mCache->addEntryToObjectCache(normalized, mWarningImage); + return mWarningImage; + } + int type = header[2]; + int depth; + if (type == 1 || type == 9) + depth = header[7]; + else + depth = header[16]; + int alphaBPP = header[17] & 0x0F; + killAlpha = depth == 16 && alphaBPP == 1; + stream->seekg(0); + } + osgDB::ReaderWriter::ReadResult result = reader->readImage(*stream, mOptions); if (!result.success()) { @@ -149,6 +172,18 @@ namespace Resource image = newImage; } } + else if (killAlpha) + { + osg::ref_ptr newImage = new osg::Image; + newImage->setFileName(image->getFileName()); + newImage->allocateImage(image->s(), image->t(), image->r(), GL_RGB, GL_UNSIGNED_BYTE); + // OSG just won't write the alpha as there's nowhere to put it. + for (int s = 0; s < image->s(); ++s) + for (int t = 0; t < image->t(); ++t) + for (int r = 0; r < image->r(); ++r) + newImage->setColor(image->getColor(s, t, r), s, t, r); + image = newImage; + } mCache->addEntryToObjectCache(normalized, image); return image; diff --git a/components/sdlutil/sdlgraphicswindow.cpp b/components/sdlutil/sdlgraphicswindow.cpp index cd5e80c31..0a1951700 100644 --- a/components/sdlutil/sdlgraphicswindow.cpp +++ b/components/sdlutil/sdlgraphicswindow.cpp @@ -118,6 +118,29 @@ void GraphicsWindowSDL2::init() setSwapInterval(_traits->vsync); + // Update traits with what we've actually been given + // Use intermediate to avoid signed/unsigned mismatch + int intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &intermediateLocation); + _traits->red = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &intermediateLocation); + _traits->green = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &intermediateLocation); + _traits->blue = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE, &intermediateLocation); + _traits->alpha = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &intermediateLocation); + _traits->depth = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_STENCIL_SIZE, &intermediateLocation); + _traits->stencil = intermediateLocation; + + SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER, &intermediateLocation); + _traits->doubleBuffer = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_MULTISAMPLEBUFFERS, &intermediateLocation); + _traits->sampleBuffers = intermediateLocation; + SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES, &intermediateLocation); + _traits->samples = intermediateLocation; + SDL_GL_MakeCurrent(oldWin, oldCtx); mValid = true; diff --git a/components/shader/shadermanager.cpp b/components/shader/shadermanager.cpp index be662990b..8523a2962 100644 --- a/components/shader/shadermanager.cpp +++ b/components/shader/shadermanager.cpp @@ -61,45 +61,39 @@ namespace Shader return true; } - bool parseIncludes(boost::filesystem::path shaderPath, std::string& source, const std::string& templateName) + // Recursively replaces include statements with the actual source of the included files. + // Adjusts #line statements accordingly and detects cyclic includes. + // includingFiles is the set of files that include this file directly or indirectly, and is intentionally not a reference to allow automatic cleanup. + static bool parseIncludes(boost::filesystem::path shaderPath, std::string& source, const std::string& fileName, int& fileNumber, std::set includingFiles) { + // An include is cyclic if it is being included by itself + if (includingFiles.insert(shaderPath/fileName).second == false) + { + Log(Debug::Error) << "Shader " << fileName << " error: Detected cyclic #includes"; + return false; + } + Misc::StringUtils::replaceAll(source, "\r\n", "\n"); - std::set includedFiles; size_t foundPos = 0; - int fileNumber = 1; while ((foundPos = source.find("#include")) != std::string::npos) { size_t start = source.find('"', foundPos); - if (start == std::string::npos || start == source.size()-1) + if (start == std::string::npos || start == source.size() - 1) { - Log(Debug::Error) << "Shader " << templateName << " error: Invalid #include"; + Log(Debug::Error) << "Shader " << fileName << " error: Invalid #include"; return false; } - size_t end = source.find('"', start+1); + size_t end = source.find('"', start + 1); if (end == std::string::npos) { - Log(Debug::Error) << "Shader " << templateName << " error: Invalid #include"; + Log(Debug::Error) << "Shader " << fileName << " error: Invalid #include"; return false; } - std::string includeFilename = source.substr(start+1, end-(start+1)); + std::string includeFilename = source.substr(start + 1, end - (start + 1)); boost::filesystem::path includePath = shaderPath / includeFilename; - boost::filesystem::ifstream includeFstream; - includeFstream.open(includePath); - if (includeFstream.fail()) - { - Log(Debug::Error) << "Shader " << templateName << " error: Failed to open include " << includePath.string(); - return false; - } - - std::stringstream buffer; - buffer << includeFstream.rdbuf(); - std::string stringRepresentation = buffer.str(); - addLineDirectivesAfterConditionalBlocks(stringRepresentation); - - // insert #line directives so we get correct line numbers in compiler errors - int includedFileNumber = fileNumber++; + // Determine the line number that will be used for the #line directive following the included source size_t lineDirectivePosition = source.rfind("#line", foundPos); int lineNumber; if (lineDirectivePosition != std::string::npos) @@ -116,16 +110,30 @@ namespace Shader } lineNumber += std::count(source.begin() + lineDirectivePosition, source.begin() + foundPos, '\n'); + // Include the file recursively + boost::filesystem::ifstream includeFstream; + includeFstream.open(includePath); + if (includeFstream.fail()) + { + Log(Debug::Error) << "Shader " << fileName << " error: Failed to open include " << includePath.string(); + return false; + } + int includedFileNumber = fileNumber++; + + std::stringstream buffer; + buffer << includeFstream.rdbuf(); + std::string stringRepresentation = buffer.str(); + if (!addLineDirectivesAfterConditionalBlocks(stringRepresentation) + || !parseIncludes(shaderPath, stringRepresentation, includeFilename, fileNumber, includingFiles)) + { + Log(Debug::Error) << "In file included from " << fileName << "." << lineNumber; + return false; + } + std::stringstream toInsert; toInsert << "#line 0 " << includedFileNumber << "\n" << stringRepresentation << "\n#line " << lineNumber << " 0\n"; - source.replace(foundPos, (end-foundPos+1), toInsert.str()); - - if (includedFiles.insert(includePath).second == false) - { - Log(Debug::Error) << "Shader " << templateName << " error: Detected cyclic #includes"; - return false; - } + source.replace(foundPos, (end - foundPos + 1), toInsert.str()); } return true; } @@ -282,21 +290,22 @@ namespace Shader TemplateMap::iterator templateIt = mShaderTemplates.find(templateName); if (templateIt == mShaderTemplates.end()) { - boost::filesystem::path p = (boost::filesystem::path(mPath) / templateName); + boost::filesystem::path path = (boost::filesystem::path(mPath) / templateName); boost::filesystem::ifstream stream; - stream.open(p); + stream.open(path); if (stream.fail()) { - Log(Debug::Error) << "Failed to open " << p.string(); + Log(Debug::Error) << "Failed to open " << path.string(); return nullptr; } std::stringstream buffer; buffer << stream.rdbuf(); // parse includes + int fileNumber = 1; std::string source = buffer.str(); if (!addLineDirectivesAfterConditionalBlocks(source) - || !parseIncludes(boost::filesystem::path(mPath), source, templateName)) + || !parseIncludes(boost::filesystem::path(mPath), source, templateName, fileNumber, {})) return nullptr; templateIt = mShaderTemplates.insert(std::make_pair(templateName, source)).first; diff --git a/docs/source/reference/modding/settings/game.rst b/docs/source/reference/modding/settings/game.rst index 46bd22e50..f942f6e7c 100644 --- a/docs/source/reference/modding/settings/game.rst +++ b/docs/source/reference/modding/settings/game.rst @@ -331,8 +331,43 @@ If enabled then the character turns lower body to the direction of movement. Upp This setting can be controlled in Advanced tab of the launcher. +smooth movement +--------------- + +:Type: boolean +:Range: True/False +:Default: False + +Makes NPCs and player movement more smooth. + +Recommended to use with "turn to movement direction" enabled. + +This setting can be controlled in Advanced tab of the launcher. + +NPCs avoid collisions +--------------------- + +:Type: boolean +:Range: True/False +:Default: False + +If enabled NPCs apply evasion maneuver to avoid collisions with others. + +This setting can be controlled in Advanced tab of the launcher. + +NPCs give way +------------- + +:Type: boolean +:Range: True/False +:Default: True + +Standing NPCs give way to moving ones. Works only if 'NPCs avoid collisions' is enabled. + +This setting can only be configured by editing the settings configuration file. + swim upward correction ----------------- +---------------------- :Type: boolean :Range: True/False diff --git a/extern/recastnavigation/CMakeLists.txt b/extern/recastnavigation/CMakeLists.txt index 4952e51da..0d31c2e36 100644 --- a/extern/recastnavigation/CMakeLists.txt +++ b/extern/recastnavigation/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.0) +# for link time optimization, remove if cmake version is >= 3.9 +if(POLICY CMP0069) + cmake_policy(SET CMP0069 NEW) +endif() + project(RecastNavigation) # lib versions diff --git a/files/settings-default.cfg b/files/settings-default.cfg index ac5433f30..fab5fe569 100644 --- a/files/settings-default.cfg +++ b/files/settings-default.cfg @@ -325,6 +325,15 @@ uncapped damage fatigue = false # Turn lower body to movement direction. 'true' makes diagonal movement more realistic. turn to movement direction = false +# Makes all movements of NPCs and player more smooth. +smooth movement = false + +# All actors avoid collisions with other actors. +NPCs avoid collisions = false + +# Give way to moving actors when idle. Requires 'NPCs avoid collisions' to be enabled. +NPCs give way = true + # Makes player swim a bit upward from the line of sight. swim upward correction = false diff --git a/files/ui/advancedpage.ui b/files/ui/advancedpage.ui index 9084a7aba..3a91db791 100644 --- a/files/ui/advancedpage.ui +++ b/files/ui/advancedpage.ui @@ -43,6 +43,16 @@ + + + + <html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html> + + + NPCs avoid collisions + + + @@ -233,6 +243,16 @@ + + + + <html><head/><body><p>Makes NPCs and player movement more smooth. Recommended to use with "turn to movement direction" enabled.</p></body></html> + + + Smooth movement + + + @@ -283,7 +303,7 @@ - + <html><head/><body><p>Load per-group KF-files and skeleton files from Animations folder</p></body></html>