diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d815c7d77..565feff14 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -34,6 +34,8 @@ MacOS: tags: - macos - xcode + except: + - branches # because our CI VMs are not public, MRs can't use them and timeout stage: build allow_failure: true script: @@ -43,4 +45,25 @@ MacOS: - cd build; make -j2 package artifacts: paths: - - build/OpenMW-*.dmg \ No newline at end of file + - build/OpenMW-*.dmg + +Windows: + tags: + - win10 + - msvc2017 + except: + - branches # because our CI VMs are not public, MRs can't use them and timeout + stage: build + allow_failure: true + script: + # - env # turn on for debugging + - sh %CI_PROJECT_DIR%/CI/before_script.msvc.sh -c Release -p x64 -v 2017 -V + - SET msBuildLocation="C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe" + - call %msBuildLocation% MSVC2017_64\OpenMW.sln /t:Build /p:Configuration=Release /m:%NUMBER_OF_PROCESSORS% + - 7z a OpenMW_MSVC2017_64_%CI_BUILD_REF_NAME%_%CI_BUILD_ID%.zip %CI_PROJECT_DIR%\MSVC2017_64\Release\ + cache: + paths: + - deps + artifacts: + paths: + - "*.zip" diff --git a/AUTHORS.md b/AUTHORS.md index f1e6e58ee..d2fd05180 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -143,6 +143,7 @@ Programmers Rohit Nirmal Roman Melnik (Kromgart) Roman Proskuryakov (kpp) + Roman Siromakha (elsid) Sandy Carter (bwrsandman) Scott Howard scrawl diff --git a/CHANGELOG.md b/CHANGELOG.md index f839279f3..7fde57f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ------ Bug #1990: Sunrise/sunset not set correct + Bug #2131: Lustidrike's spell misses the player every time Bug #2222: Fatigue's effect on selling price is backwards Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped Bug #2455: Creatures attacks degrade armor @@ -11,6 +12,7 @@ Bug #2852: No murder bounty when a player follower commits murder Bug #2862: [macOS] Can't quit launcher using Command-Q or OpenMW->Quit Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y + Bug #3249: Fixed revert function not updating views properly Bug #3374: Touch spells not hitting kwama foragers Bug #3486: [Mod] NPC Commands does not work Bug #3591: Angled hit distance too low @@ -22,15 +24,18 @@ Bug #3997: Almalexia doesn't pace Bug #4036: Weird behaviour of AI packages if package target has non-unique ID Bug #4047: OpenMW not reporting its version number in MacOS; OpenMW-CS not doing it fully + Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts Bug #4125: OpenMW logo cropped on bugtracker Bug #4215: OpenMW shows book text after last EOL tag Bug #4221: Characters get stuck in V-shaped terrain Bug #4251: Stationary NPCs do not return to their position after combat + Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+ Bug #4286: Scripted animations can be interrupted Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4327: Missing animations during spell/weapon stance switching + Bug #4358: Running animation is interrupted when magic mode is toggled Bug #4368: Settings window ok button doesn't have key focus by default Bug #4393: NPCs walk back to where they were after using ResetActors Bug #4416: Handle exception if we try to play non-music file @@ -47,12 +52,23 @@ Bug #4457: Item without CanCarry flag prevents shield autoequipping in dark areas Bug #4458: AiWander console command handles idle chances incorrectly Bug #4459: NotCell dialogue condition doesn't support partial matches + Bug #4460: Script function "Equip" doesn't bypass beast restrictions Bug #4461: "Open" spell from non-player caster isn't a crime + Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal Bug #4474: No fallback when getVampireHead fails Bug #4475: Scripted animations should not cause movement Bug #4479: "Game" category on Advanced page is getting too long - Bug #4480: Segfalt in QuickKeysMenu when item no longer in inventory + Bug #4480: Segfault in QuickKeysMenu when item no longer in inventory + Bug #4489: Goodbye doesn't block dialogue hyperlinks + Bug #4490: PositionCell on player gives "Error: tried to add local script twice" + Bug #4491: Training cap based off Base Skill instead of Modified Skill + Bug #4495: Crossbow animations blending is buggy + Bug #4496: SpellTurnLeft and SpellTurnRight animation groups are unused + Bug #4497: File names starting with x or X are not classified as animation + Bug #4503: Cast and ExplodeSpell commands increase alteration skill + Feature #2606: Editor: Implemented (optional) case sensitive global search + Feature #3083: Play animation when NPC is casting spell via script Feature #3276: Editor: Search- Show number of (remaining) search results and indicate a search without any results Feature #3641: Editor: Limit FPS in 3d preview window Feature #4222: 360° screenshots @@ -62,6 +78,8 @@ Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically Feature #4444: Per-group KF-animation files support Feature #4466: Editor: Add option to ignore "Base" records when running verifier + Feature #4012: Editor: Write a log file if OpenCS crashes + Task #2490: Don't open command prompt window on Release-mode builds automatically 0.44.0 ------ diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh index 38b304bf9..264d33dd2 100644 --- a/CI/before_script.msvc.sh +++ b/CI/before_script.msvc.sh @@ -1,4 +1,5 @@ #!/bin/bash +# set -x # turn-on for debugging MISSINGTOOLS=0 @@ -76,7 +77,6 @@ while [ $# -gt 0 ]; do h ) cat < Set the configuration, can also be set with environment variable CONFIGURATION. @@ -232,10 +232,9 @@ fi case $VS_VERSION in 15|15.0|2017 ) GENERATOR="Visual Studio 15 2017" - TOOLSET="vc140" - TOOLSET_REAL="vc141" + TOOLSET="vc141" MSVC_REAL_VER="15" - MSVC_VER="14" + MSVC_VER="14.1" MSVC_YEAR="2015" MSVC_DISPLAY_YEAR="2017" ;; @@ -243,9 +242,8 @@ case $VS_VERSION in 14|14.0|2015 ) GENERATOR="Visual Studio 14 2015" TOOLSET="vc140" - TOOLSET_REAL="vc140" MSVC_REAL_VER="14" - MSVC_VER="14" + MSVC_VER="14.0" MSVC_YEAR="2015" MSVC_DISPLAY_YEAR="2015" ;; @@ -253,9 +251,8 @@ case $VS_VERSION in 12|12.0|2013 ) GENERATOR="Visual Studio 12 2013" TOOLSET="vc120" - TOOLSET_REAL="vc120" MSVC_REAL_VER="12" - MSVC_VER="12" + MSVC_VER="12.0" MSVC_YEAR="2013" MSVC_DISPLAY_YEAR="2013" ;; @@ -325,9 +322,9 @@ if [ -z $SKIP_DOWNLOAD ]; then # Boost if [ -z $APPVEYOR ]; then - download "Boost 1.61.0" \ - "https://sourceforge.net/projects/boost/files/boost-binaries/1.61.0/boost_1_61_0-msvc-${MSVC_VER}.0-${BITS}.exe" \ - "boost-1.61.0-msvc${MSVC_YEAR}-win${BITS}.exe" + download "Boost 1.67.0" \ + "https://sourceforge.net/projects/boost/files/boost-binaries/1.67.0/boost_1_67_0-msvc-${MSVC_VER}-${BITS}.exe" \ + "boost-1.67.0-msvc${MSVC_YEAR}-win${BITS}.exe" fi # Bullet @@ -365,8 +362,8 @@ if [ -z $SKIP_DOWNLOAD ]; then QT_SUFFIX="" fi - download "Qt 5.7.2" \ - "https://download.qt.io/official_releases/qt/5.7/5.7.0/qt-opensource-windows-x86-msvc${MSVC_YEAR}${QT_SUFFIX}-5.7.0.exe" \ + download "Qt 5.7.0" \ + "https://download.qt.io/archive/qt/5.7/5.7.0/qt-opensource-windows-x86-msvc${MSVC_YEAR}${QT_SUFFIX}-5.7.0.exe" \ "qt-5.7.0-msvc${MSVC_YEAR}-win${BITS}.exe" \ "https://www.lysator.liu.se/~ace/OpenMW/deps/qt-5-install.qs" \ "qt-5-install.qs" @@ -403,9 +400,9 @@ echo # Boost if [ -z $APPVEYOR ]; then - printf "Boost 1.61.0... " + printf "Boost 1.67.0... " else - if [ $MSVC_VER -eq 12 ]; then + if [ $MSVC_VER -eq 12.0 ]; then printf "Boost 1.58.0 AppVeyor... " else printf "Boost 1.67.0 AppVeyor... " @@ -417,17 +414,27 @@ fi BOOST_SDK="$(real_pwd)/Boost" - if [ -d Boost ] && grep "BOOST_VERSION 106100" Boost/boost/version.hpp > /dev/null; then + # Boost's installer is still based on ms-dos API that doesn't support larger than 260 char path names + # We work around this by installing to root of the current working drive and then move it to our deps + # get the current working drive's root, we'll install to that temporarily + CWD_DRIVE_ROOT="$(powershell -command '(get-location).Drive.Root')Boost_temp" + CWD_DRIVE_ROOT_BASH=$(echo "$CWD_DRIVE_ROOT" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,") + if [ -d CWD_DRIVE_ROOT_BASH ]; then + printf "Cannot continue, ${CWD_DRIVE_ROOT_BASH} aka ${CWD_DRIVE_ROOT} already exists. Please remove before re-running. "; + exit 1; + fi + + if [ -d ${BOOST_SDK} ] && grep "BOOST_VERSION 106700" Boost/boost/version.hpp > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf Boost - "${DEPS}/boost-1.61.0-msvc${MSVC_YEAR}-win${BITS}.exe" //dir="$(echo $BOOST_SDK | sed s,/,\\\\,g)" //verysilent + [ -n "$CI" ] && CI_EXTRA_INNO_OPTIONS="//SUPPRESSMSGBOXES //LOG='boost_install.log'" + "${DEPS}/boost-1.67.0-msvc${MSVC_YEAR}-win${BITS}.exe" //DIR="${CWD_DRIVE_ROOT}" //VERYSILENT //NORESTART ${CI_EXTRA_INNO_OPTIONS} + mv "${CWD_DRIVE_ROOT_BASH}" "${BOOST_SDK}" fi - add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ - -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.0" + -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}" add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" - echo Done. else # Appveyor unstable has all the boost we need already @@ -444,19 +451,17 @@ fi add_cmake_opts -DBOOST_ROOT="$BOOST_SDK" \ -DBOOST_LIBRARYDIR="${BOOST_SDK}/lib${BITS}-msvc-${MSVC_VER}.${LIB_SUFFIX}" - add_cmake_opts -DBoost_COMPILER="-${TOOLSET_REAL}" + add_cmake_opts -DBoost_COMPILER="-${TOOLSET}" echo Done. fi } cd $DEPS echo - # Bullet printf "Bullet 2.86... " { cd $DEPS_INSTALL - if [ -d Bullet ]; then printf -- "Exists. (No version checking) " elif [ -z $SKIP_EXTRACT ]; then @@ -464,49 +469,38 @@ printf "Bullet 2.86... " eval 7z x -y "${DEPS}/Bullet-2.86-msvc${MSVC_YEAR}-win${BITS}.7z" $STRIP mv "Bullet-2.86-msvc${MSVC_YEAR}-win${BITS}" Bullet fi - export BULLET_ROOT="$(real_pwd)/Bullet" - echo Done. } cd $DEPS echo - # FFmpeg printf "FFmpeg 3.2.4... " { cd $DEPS_INSTALL - if [ -d FFmpeg ] && grep "FFmpeg version: 3.2.4" FFmpeg/README.txt > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf FFmpeg - eval 7z x -y "${DEPS}/ffmpeg-3.2.4-win${BITS}.zip" $STRIP eval 7z x -y "${DEPS}/ffmpeg-3.2.4-dev-win${BITS}.zip" $STRIP - mv "ffmpeg-3.2.4-win${BITS}-shared" FFmpeg cp -r "ffmpeg-3.2.4-win${BITS}-dev/"* FFmpeg/ rm -rf "ffmpeg-3.2.4-win${BITS}-dev" fi - export FFMPEG_HOME="$(real_pwd)/FFmpeg" add_runtime_dlls "$(pwd)/FFmpeg/bin/"{avcodec-57,avformat-57,avutil-55,swresample-2,swscale-4}.dll - if [ $BITS -eq 32 ]; then add_cmake_opts "-DCMAKE_EXE_LINKER_FLAGS=\"/machine:X86 /safeseh:no\"" fi - echo Done. } cd $DEPS echo - # MyGUI printf "MyGUI 3.2.2... " { cd $DEPS_INSTALL - if [ -d MyGUI ] && \ grep "MYGUI_VERSION_MAJOR 3" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ grep "MYGUI_VERSION_MINOR 2" MyGUI/include/MYGUI/MyGUI_Prerequest.h > /dev/null && \ @@ -518,21 +512,17 @@ printf "MyGUI 3.2.2... " eval 7z x -y "${DEPS}/MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}.7z" $STRIP mv "MyGUI-3.2.2-msvc${MSVC_YEAR}-win${BITS}" MyGUI fi - export MYGUI_HOME="$(real_pwd)/MyGUI" - if [ $CONFIGURATION == "Debug" ]; then SUFFIX="_d" else SUFFIX="" fi add_runtime_dlls "$(pwd)/MyGUI/bin/${CONFIGURATION}/MyGUIEngine${SUFFIX}.dll" - echo Done. } cd $DEPS echo - # OpenAL printf "OpenAL-Soft 1.17.2... " { @@ -542,24 +532,18 @@ printf "OpenAL-Soft 1.17.2... " rm -rf openal-soft-1.17.2-bin eval 7z x -y OpenAL-Soft-1.17.2.zip $STRIP fi - OPENAL_SDK="$(real_pwd)/openal-soft-1.17.2-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.17.2-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll" - echo Done. } cd $DEPS echo - # OSG printf "OSG 3.4.1-scrawl... " { cd $DEPS_INSTALL - if [ -d OSG ] && \ grep "OPENSCENEGRAPH_MAJOR_VERSION 3" OSG/include/osg/Version > /dev/null && \ grep "OPENSCENEGRAPH_MINOR_VERSION 4" OSG/include/osg/Version > /dev/null && \ @@ -571,28 +555,21 @@ printf "OSG 3.4.1-scrawl... " eval 7z x -y "${DEPS}/OSG-3.4.1-scrawl-msvc${MSVC_YEAR}-win${BITS}.7z" $STRIP mv "OSG-3.4.1-scrawl-msvc${MSVC_YEAR}-win${BITS}" OSG 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}${SUFFIX}.dll - add_osg_dlls "$(pwd)/OSG/bin/osgPlugins-3.4.1/osgdb_"{bmp,dds,jpeg,osg,png,tga}${SUFFIX}.dll add_osg_dlls "$(pwd)/OSG/bin/osgPlugins-3.4.1/osgdb_serializers_osg"{,animation,fx,ga,particle,text,util,viewer}${SUFFIX}.dll - echo Done. } cd $DEPS echo - # Qt if [ -z $APPVEYOR ]; then printf "Qt 5.7.0... " @@ -605,71 +582,53 @@ fi else SUFFIX="" fi - if [ -z $APPVEYOR ]; then cd $DEPS_INSTALL QT_SDK="$(real_pwd)/Qt/5.7/msvc${MSVC_YEAR}${SUFFIX}" - if [ -d Qt ] && head -n2 Qt/InstallationLog.txt | grep "5.7.0" > /dev/null; then printf "Exists. " elif [ -z $SKIP_EXTRACT ]; then rm -rf Qt cp "${DEPS}/qt-5-install.qs" qt-install.qs - - sed -i "s|INSTALL_DIR|$(real_pwd)/Qt|" qt-install.qs sed -i "s/qt.VERSION.winBITS_msvcYEAR/qt.57.win${BITS}_msvc${MSVC_YEAR}${SUFFIX}/" qt-install.qs - printf -- "(Installation might take a while) " "${DEPS}/qt-5.7.0-msvc${MSVC_YEAR}-win${BITS}.exe" --script qt-install.qs --silent - mv qt-install.qs Qt/ - echo Done. printf " Cleaning up extraneous data... " rm -r "$(real_pwd)/Qt/"{dist,Docs,Examples,Tools,vcredist,components.xml,MaintenanceTool.dat,MaintenanceTool.exe,MaintenanceTool.ini,network.xml,qt-install.qs} fi - cd $QT_SDK - add_cmake_opts -DDESIRED_QT_VERSION=5 \ -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" - echo Done. else QT_SDK="C:/Qt/5.10/msvc${MSVC_DISPLAY_YEAR}${SUFFIX}" - add_cmake_opts -DDESIRED_QT_VERSION=5 \ -DQT_QMAKE_EXECUTABLE="${QT_SDK}/bin/qmake.exe" \ -DCMAKE_PREFIX_PATH="$QT_SDK" - if [ $CONFIGURATION == "Debug" ]; then SUFFIX="d" else SUFFIX="" fi - DIR=$(echo "${QT_SDK}" | sed "s,\\\\,/,g" | sed "s,\(.\):,/\\1,") - add_runtime_dlls "${DIR}/bin/Qt5"{Core,Gui,Network,OpenGL,Widgets}${SUFFIX}.dll add_qt_platform_dlls "${DIR}/plugins/platforms/qwindows${SUFFIX}.dll" - echo Done. fi } cd $DEPS echo - # SDL2 printf "SDL 2.0.7... " { @@ -679,26 +638,18 @@ printf "SDL 2.0.7... " rm -rf SDL2-2.0.7 eval 7z x -y SDL2-2.0.7.zip $STRIP fi - export SDL2DIR="$(real_pwd)/SDL2-2.0.7" - add_runtime_dlls "$(pwd)/SDL2-2.0.7/lib/x${ARCHSUFFIX}/SDL2.dll" - echo Done. } echo - - cd $DEPS_INSTALL/.. - echo echo "Setting up OpenMW build..." - add_cmake_opts -DBUILD_BSATOOL=no \ -DBUILD_ESMTOOL=no \ -DBUILD_MYGUI_PLUGIN=no \ -DOPENMW_MP_BUILD=on - if [ ! -z $CI ]; then case $STEP in components ) @@ -710,7 +661,6 @@ if [ ! -z $CI ]; then -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; - openmw ) echo " Building subproject: OpenMW." add_cmake_opts -DBUILD_ESSIMPORTER=no \ @@ -719,7 +669,6 @@ if [ ! -z $CI ]; then -DBUILD_OPENCS=no \ -DBUILD_WIZARD=no ;; - opencs ) echo " Building subproject: OpenCS." add_cmake_opts -DBUILD_ESSIMPORTER=no \ @@ -728,7 +677,6 @@ if [ ! -z $CI ]; then -DBUILD_OPENMW=no \ -DBUILD_WIZARD=no ;; - misc ) echo " Building subprojects: Misc." add_cmake_opts -DBUILD_OPENCS=no \ @@ -736,7 +684,6 @@ if [ ! -z $CI ]; then ;; esac fi - # NOTE: Disable this when/if we want to run test cases #if [ -z $CI ]; then echo "- Copying Runtime DLLs..." @@ -745,16 +692,13 @@ fi TARGET="$(basename "$DLL")" if [[ "$DLL" == *":"* ]]; then IFS=':'; SPLIT=( ${DLL} ); unset IFS - DLL=${SPLIT[0]} TARGET=${SPLIT[1]} fi - echo " ${TARGET}." cp "$DLL" "$BUILD_CONFIG/$TARGET" done echo - echo "- OSG Plugin DLLs..." mkdir -p $BUILD_CONFIG/osgPlugins-3.4.1 for DLL in $OSG_PLUGINS; do @@ -762,7 +706,6 @@ fi cp "$DLL" $BUILD_CONFIG/osgPlugins-3.4.1 done echo - echo "- Qt Platform DLLs..." mkdir -p ${BUILD_CONFIG}/platforms for DLL in $QT_PLATFORMS; do @@ -771,16 +714,13 @@ fi done echo #fi - if [ -z $VERBOSE ]; then printf -- "- Configuring... " else echo "- cmake .. $CMAKE_OPTS" fi - run_cmd cmake .. $CMAKE_OPTS RET=$? - if [ -z $VERBOSE ]; then if [ $RET -eq 0 ]; then echo Done. @@ -788,5 +728,4 @@ if [ -z $VERBOSE ]; then echo Failed. fi fi - exit $RET diff --git a/CMakeLists.txt b/CMakeLists.txt index 6799182be..4cfbc64ef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -666,10 +666,10 @@ if (WIN32) endif() if (BUILD_OPENMW) - # Release builds use the debug console - set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:CONSOLE") - set_target_properties(tes3mp PROPERTIES COMPILE_DEFINITIONS_RELEASE "_CONSOLE") - set_target_properties(tes3mp PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:CONSOLE") + # Release builds don't use the debug console + set_target_properties(tes3mp PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS") + set_target_properties(tes3mp PROPERTIES COMPILE_DEFINITIONS_RELEASE "_WINDOWS") + set_target_properties(tes3mp PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS") endif() # Play a bit with the warning levels @@ -680,7 +680,8 @@ if (WIN32) # Warnings that aren't enabled normally and don't need to be enabled # They're unneeded and sometimes completely retarded warnings that /Wall enables # Not going to bother commenting them as they tend to warn on every standard library file - 4061 4263 4264 4266 4350 4371 4435 4514 4548 4571 4610 4619 4623 4625 4626 4628 4640 4668 4710 4711 4820 4826 4917 4946 + 4061 4263 4264 4266 4350 4371 4435 4514 4548 4571 4610 4619 4623 4625 + 4626 4628 4640 4668 4710 4711 4768 4820 4826 4917 4946 5032 5039 5045 # Warnings that are thrown on standard libraries and not OpenMW 4347 # Non-template function with same name and parameter count as template function @@ -701,6 +702,7 @@ if (WIN32) # caused by MyGUI 4275 # non dll-interface class 'std::exception' used as base for dll-interface class 'MyGUI::Exception' + 4297 # function assumed not to throw an exception but does # OpenMW specific warnings 4099 # Type mismatch, declared class or struct is defined with other type diff --git a/apps/mwiniimporter/importer.cpp b/apps/mwiniimporter/importer.cpp index 6f5c2e2cd..4fbe97720 100644 --- a/apps/mwiniimporter/importer.cpp +++ b/apps/mwiniimporter/importer.cpp @@ -874,7 +874,6 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co { std::vector> contentFiles; std::string baseGameFile("Game Files:GameFile"); - std::string gameFile(""); std::time_t defaultTime = 0; ToUTF8::Utf8Encoder encoder(mEncoding); @@ -890,7 +889,7 @@ void MwIniImporter::importGameFiles(multistrmap &cfg, const multistrmap &ini, co multistrmap::const_iterator it = ini.begin(); for (int i=0; it != ini.end(); i++) { - gameFile = baseGameFile; + std::string gameFile = baseGameFile; gameFile.append(std::to_string(i)); it = ini.find(gameFile); diff --git a/apps/opencs/editor.cpp b/apps/opencs/editor.cpp index 35ce51337..6ca10e0f6 100644 --- a/apps/opencs/editor.cpp +++ b/apps/opencs/editor.cpp @@ -5,6 +5,9 @@ #include #include + +#include + #include #include @@ -18,12 +21,16 @@ using namespace Fallback; -CS::Editor::Editor () +CS::Editor::Editor (int argc, char **argv) : mSettingsState (mCfgMgr), mDocumentManager (mCfgMgr), mViewManager (mDocumentManager), mPid(""), mLock(), mMerge (mDocumentManager), mIpcServerName ("org.openmw.OpenCS"), mServer(NULL), mClientSocket(NULL) -{ +{ + // install the crash handler as soon as possible. note that the log path + // does not depend on config being read. + crashCatcherInstall(argc, argv, (mCfgMgr.getLogPath() / "openmw-cs-crash.log").string()); + std::pair > config = readConfig(); setupDataFiles (config.first); diff --git a/apps/opencs/editor.hpp b/apps/opencs/editor.hpp index b60f5c6a8..21199e2ca 100644 --- a/apps/opencs/editor.hpp +++ b/apps/opencs/editor.hpp @@ -66,7 +66,7 @@ namespace CS public: - Editor (); + Editor (int argc, char **argv); ~Editor (); bool makeIPCServer(); diff --git a/apps/opencs/main.cpp b/apps/opencs/main.cpp index fc5e8fc7a..d599f1f96 100644 --- a/apps/opencs/main.cpp +++ b/apps/opencs/main.cpp @@ -48,7 +48,7 @@ int main(int argc, char *argv[]) #endif try - { + { // To allow background thread drawing in OSG QApplication::setAttribute(Qt::AA_X11InitThreads, true); @@ -67,7 +67,7 @@ int main(int argc, char *argv[]) application.setWindowIcon (QIcon (":./openmw-cs.png")); - CS::Editor editor; + CS::Editor editor(argc, argv); if(!editor.makeIPCServer()) { diff --git a/apps/opencs/model/tools/search.cpp b/apps/opencs/model/tools/search.cpp index 0c068ba11..7cf89f3b5 100644 --- a/apps/opencs/model/tools/search.cpp +++ b/apps/opencs/model/tools/search.cpp @@ -22,7 +22,8 @@ void CSMTools::Search::searchTextCell (const CSMWorld::IdTableBase *model, int pos = 0; - while ((pos = text.indexOf (search, pos, Qt::CaseInsensitive))!=-1) + Qt::CaseSensitivity caseSensitivity = mCase ? Qt::CaseSensitive : Qt::CaseInsensitive; + while ((pos = text.indexOf (search, pos, caseSensitivity))!=-1) { std::ostringstream hint; hint @@ -120,25 +121,26 @@ QString CSMTools::Search::flatten (const QString& text) const return flat; } -CSMTools::Search::Search() : mType (Type_None), mValue (0), mIdColumn (0), mTypeColumn (0), +CSMTools::Search::Search() : mType (Type_None), mValue (0), mCase (false), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) {} -CSMTools::Search::Search (Type type, const std::string& value) -: mType (type), mText (value), mValue (0), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search (Type type, bool caseSensitive, const std::string& value) +: mType (type), mText (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { if (type!=Type_Text && type!=Type_Id) throw std::logic_error ("Invalid search parameter (string)"); } -CSMTools::Search::Search (Type type, const QRegExp& value) -: mType (type), mRegExp (value), mValue (0), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search (Type type, bool caseSensitive, const QRegExp& value) +: mType (type), mRegExp (value), mValue (0), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { + mRegExp.setCaseSensitivity(mCase ? Qt::CaseSensitive : Qt::CaseInsensitive); if (type!=Type_TextRegEx && type!=Type_IdRegEx) throw std::logic_error ("Invalid search parameter (RegExp)"); } -CSMTools::Search::Search (Type type, int value) -: mType (type), mValue (value), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) +CSMTools::Search::Search (Type type, bool caseSensitive, int value) +: mType (type), mValue (value), mCase (caseSensitive), mIdColumn (0), mTypeColumn (0), mPaddingBefore (10), mPaddingAfter (10) { if (type!=Type_RecordState) throw std::logic_error ("invalid search parameter (int)"); diff --git a/apps/opencs/model/tools/search.hpp b/apps/opencs/model/tools/search.hpp index 69b98bbdb..35cfa6402 100644 --- a/apps/opencs/model/tools/search.hpp +++ b/apps/opencs/model/tools/search.hpp @@ -43,6 +43,7 @@ namespace CSMTools std::string mText; QRegExp mRegExp; int mValue; + bool mCase; std::set mColumns; int mIdColumn; int mTypeColumn; @@ -67,11 +68,11 @@ namespace CSMTools Search(); - Search (Type type, const std::string& value); + Search (Type type, bool caseSensitive, const std::string& value); - Search (Type type, const QRegExp& value); + Search (Type type, bool caseSensitive, const QRegExp& value); - Search (Type type, int value); + Search (Type type, bool caseSensitive, int value); // Configure search for the specified model. void configure (const CSMWorld::IdTableBase *model); diff --git a/apps/opencs/model/world/columnimp.hpp b/apps/opencs/model/world/columnimp.hpp index 4ad447b0a..cd2a4c79e 100644 --- a/apps/opencs/model/world/columnimp.hpp +++ b/apps/opencs/model/world/columnimp.hpp @@ -136,7 +136,7 @@ namespace CSMWorld struct VarTypeColumn : public Column { VarTypeColumn (ColumnBase::Display display) - : Column (Columns::ColumnId_ValueType, display) + : Column (Columns::ColumnId_ValueType, display, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) {} virtual QVariant get (const Record& record) const @@ -161,7 +161,7 @@ namespace CSMWorld template struct VarValueColumn : public Column { - VarValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Var) {} + VarValueColumn() : Column (Columns::ColumnId_Value, ColumnBase::Display_Var, ColumnBase::Flag_Table | ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh) {} virtual QVariant get (const Record& record) const { diff --git a/apps/opencs/model/world/idtable.cpp b/apps/opencs/model/world/idtable.cpp index 3e503a80c..cb48fc85f 100644 --- a/apps/opencs/model/world/idtable.cpp +++ b/apps/opencs/model/world/idtable.cpp @@ -84,15 +84,28 @@ bool CSMWorld::IdTable::setData (const QModelIndex &index, const QVariant &value if (mIdCollection->getColumn (index.column()).isEditable() && role==Qt::EditRole) { mIdCollection->setData (index.row(), index.column(), value); - emit dataChanged(index, index); - // Modifying a value can also change the Modified status of a record. int stateColumn = searchColumnIndex(Columns::ColumnId_Modification); if (stateColumn != -1) { - QModelIndex stateIndex = this->index(index.row(), stateColumn); - emit dataChanged(stateIndex, stateIndex); - } + if (index.column() == stateColumn) + { + // modifying the state column can modify other values. we need to tell + // views that the whole row has changed. + + emit dataChanged(this->index(index.row(), 0), + this->index(index.row(), columnCount(index.parent()))); + + } else + { + emit dataChanged(index, index); + + // Modifying a value can also change the Modified status of a record. + QModelIndex stateIndex = this->index(index.row(), stateColumn); + emit dataChanged(stateIndex, stateIndex); + } + } else + emit dataChanged(index, index); return true; } diff --git a/apps/opencs/model/world/record.hpp b/apps/opencs/model/world/record.hpp index 0313f2e41..3362f9f96 100644 --- a/apps/opencs/model/world/record.hpp +++ b/apps/opencs/model/world/record.hpp @@ -156,4 +156,4 @@ namespace CSMWorld } } -#endif \ No newline at end of file +#endif diff --git a/apps/opencs/view/doc/view.cpp b/apps/opencs/view/doc/view.cpp index 10de46e06..9e87e9a3a 100644 --- a/apps/opencs/view/doc/view.cpp +++ b/apps/opencs/view/doc/view.cpp @@ -101,15 +101,39 @@ void CSVDoc::View::setupFileMenu() file->addAction(exit); } +namespace +{ + + void updateUndoRedoAction(QAction *action, const std::string &settingsKey) + { + QKeySequence seq; + CSMPrefs::State::get().getShortcutManager().getSequence(settingsKey, seq); + action->setShortcut(seq); + } + +} + +void CSVDoc::View::undoActionChanged() +{ + updateUndoRedoAction(mUndo, "document-edit-undo"); +} + +void CSVDoc::View::redoActionChanged() +{ + updateUndoRedoAction(mRedo, "document-edit-redo"); +} + void CSVDoc::View::setupEditMenu() { QMenu *edit = menuBar()->addMenu (tr ("Edit")); mUndo = mDocument->getUndoStack().createUndoAction (this, tr("Undo")); setupShortcut("document-edit-undo", mUndo); + connect(mUndo, SIGNAL (changed ()), this, SLOT (undoActionChanged ())); edit->addAction (mUndo); - mRedo= mDocument->getUndoStack().createRedoAction (this, tr("Redo")); + mRedo = mDocument->getUndoStack().createRedoAction (this, tr("Redo")); + connect(mRedo, SIGNAL (changed ()), this, SLOT (redoActionChanged ())); setupShortcut("document-edit-redo", mRedo); edit->addAction (mRedo); diff --git a/apps/opencs/view/doc/view.hpp b/apps/opencs/view/doc/view.hpp index 46aa87891..5418b7720 100644 --- a/apps/opencs/view/doc/view.hpp +++ b/apps/opencs/view/doc/view.hpp @@ -152,6 +152,10 @@ namespace CSVDoc void settingChanged (const CSMPrefs::Setting *setting); + void undoActionChanged(); + + void redoActionChanged(); + void newView(); void save(); diff --git a/apps/opencs/view/render/object.cpp b/apps/opencs/view/render/object.cpp index dae6467c5..204bf4c2f 100644 --- a/apps/opencs/view/render/object.cpp +++ b/apps/opencs/view/render/object.cpp @@ -372,10 +372,10 @@ osg::ref_ptr CSVRender::Object::makeRotateMarker (int axis) indices[j] -= VertexCount; } - size_t offset = i * IndicesPerSegment; + size_t elementOffset = i * IndicesPerSegment; for (size_t j = 0; j < IndicesPerSegment; ++j) { - primitives->setElement(offset++, indices[IndexPattern[j]]); + primitives->setElement(elementOffset++, indices[IndexPattern[j]]); } } diff --git a/apps/opencs/view/render/worldspacewidget.cpp b/apps/opencs/view/render/worldspacewidget.cpp index 084fb87e6..5b825fab1 100644 --- a/apps/opencs/view/render/worldspacewidget.cpp +++ b/apps/opencs/view/render/worldspacewidget.cpp @@ -651,7 +651,6 @@ void CSVRender::WorldspaceWidget::mouseMoveEvent (QMouseEvent *event) if (mDragMode == InteractionType_PrimaryEdit) { - EditMode& editMode = dynamic_cast (*mEditMode->getCurrent()); editMode.drag (event->pos(), mDragX, mDragY, mDragFactor); // note: terraintexturemode only uses pos } } diff --git a/apps/opencs/view/tools/searchbox.cpp b/apps/opencs/view/tools/searchbox.cpp index d98044760..87a8be574 100644 --- a/apps/opencs/view/tools/searchbox.cpp +++ b/apps/opencs/view/tools/searchbox.cpp @@ -35,7 +35,7 @@ void CSVTools::SearchBox::updateSearchButton() } CSVTools::SearchBox::SearchBox (QWidget *parent) -: QWidget (parent), mSearch ("Search"), mSearchEnabled (false), mReplace ("Replace All") +: QWidget (parent), mSearch (tr("Search")), mSearchEnabled (false), mReplace (tr("Replace All")) { mLayout = new QGridLayout (this); @@ -48,29 +48,26 @@ CSVTools::SearchBox::SearchBox (QWidget *parent) ++iter) mRecordState.addItem (QString::fromUtf8 (iter->c_str())); - mMode.addItem ("Text"); - mMode.addItem ("Text (RegEx)"); - mMode.addItem ("ID"); - mMode.addItem ("ID (RegEx)"); - mMode.addItem ("Record State"); - + mMode.addItem (tr("Text")); + mMode.addItem (tr("Text (RegEx)")); + mMode.addItem (tr("ID")); + mMode.addItem (tr("ID (RegEx)")); + mMode.addItem (tr("Record State")); + connect (&mMode, SIGNAL (activated (int)), this, SLOT (modeSelected (int))); mLayout->addWidget (&mMode, 0, 0); - mLayout->addWidget (&mSearch, 0, 3); - + connect (&mText, SIGNAL (textChanged (const QString&)), this, SLOT (textChanged (const QString&))); + connect (&mText, SIGNAL (returnPressed()), this, SLOT (startSearch())); mInput.insertWidget (0, &mText); - mInput.insertWidget (1, &mRecordState); - mLayout->addWidget (&mInput, 0, 1); - - connect (&mMode, SIGNAL (activated (int)), this, SLOT (modeSelected (int))); + mInput.insertWidget (1, &mRecordState); + mLayout->addWidget (&mInput, 0, 1); - connect (&mText, SIGNAL (textChanged (const QString&)), - this, SLOT (textChanged (const QString&))); + mCaseSensitive.setText (tr ("Case")); + mLayout->addWidget (&mCaseSensitive, 0, 2); connect (&mSearch, SIGNAL (clicked (bool)), this, SLOT (startSearch (bool))); - - connect (&mText, SIGNAL (returnPressed()), this, SLOT (startSearch())); + mLayout->addWidget (&mSearch, 0, 3); // replace panel mReplaceInput.insertWidget (0, &mReplaceText); @@ -102,23 +99,24 @@ void CSVTools::SearchBox::setSearchMode (bool enabled) CSMTools::Search CSVTools::SearchBox::getSearch() const { - CSMTools::Search::Type type = static_cast (mMode.currentIndex()); - + CSMTools::Search::Type type = static_cast (mMode.currentIndex()); + bool caseSensitive = mCaseSensitive.isChecked(); + switch (type) { case CSMTools::Search::Type_Text: case CSMTools::Search::Type_Id: - return CSMTools::Search (type, std::string (mText.text().toUtf8().data())); + return CSMTools::Search (type, caseSensitive, std::string (mText.text().toUtf8().data())); case CSMTools::Search::Type_TextRegEx: case CSMTools::Search::Type_IdRegEx: - return CSMTools::Search (type, QRegExp (mText.text().toUtf8().data(), Qt::CaseInsensitive)); + return CSMTools::Search (type, caseSensitive, QRegExp (mText.text().toUtf8().data(), Qt::CaseInsensitive)); case CSMTools::Search::Type_RecordState: - return CSMTools::Search (type, mRecordState.currentIndex()); + return CSMTools::Search (type, caseSensitive, mRecordState.currentIndex()); case CSMTools::Search::Type_None: diff --git a/apps/opencs/view/tools/searchbox.hpp b/apps/opencs/view/tools/searchbox.hpp index fe56966d1..eff5296b4 100644 --- a/apps/opencs/view/tools/searchbox.hpp +++ b/apps/opencs/view/tools/searchbox.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,7 @@ namespace CSVTools QStackedWidget mInput; QLineEdit mText; QComboBox mRecordState; + QCheckBox mCaseSensitive; QPushButton mSearch; QGridLayout *mLayout; QComboBox mMode; diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index de344409a..d98ba2306 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -11,12 +11,10 @@ if (ANDROID) set(GAME ${GAME} android_main.c) endif() -if(NOT WIN32 AND NOT ANDROID) - set(GAME ${GAME} crashcatcher.cpp) -endif() set(GAME_HEADER engine.hpp ) + source_group(game FILES ${GAME} ${GAME_HEADER}) add_openmw_dir (mwrender @@ -83,9 +81,9 @@ add_openmw_dir (mwclass add_openmw_dir (mwmechanics mechanicsmanagerimp stat creaturestats magiceffects movement actorutil drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aibreathe - aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting + aicast aiescort aiface aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning - character actors objects aistate coordinateconverter trading aiface weaponpriority spellpriority + character actors objects aistate coordinateconverter trading weaponpriority spellpriority ) add_openmw_dir (mwstate diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 8346348ea..f3cc912c3 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -757,28 +757,19 @@ void OMW::Engine::go() std::cout << "OSG version: " << osgGetVersion() << std::endl; - mViewer = new osgViewer::Viewer; - mViewer->setReleaseContextAtEndOfFrameHint(false); - - osg::ref_ptr statshandler = new osgViewer::StatsHandler; - statshandler->setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3); - - statshandler->addUserStatsLine("Script", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), - "script_time_taken", 1000.0, true, false, "script_time_begin", "script_time_end", 10000); - statshandler->addUserStatsLine("Mechanics", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), - "mechanics_time_taken", 1000.0, true, false, "mechanics_time_begin", "mechanics_time_end", 10000); - statshandler->addUserStatsLine("Physics", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), - "physics_time_taken", 1000.0, true, false, "physics_time_begin", "physics_time_end", 10000); - - mViewer->addEventHandler(statshandler); - - mViewer->addEventHandler(new Resource::StatsHandler); - + // Load settings Settings::Manager settings; std::string settingspath; - settingspath = loadSettings (settings); + // Create encoder + ToUTF8::Utf8Encoder encoder (mEncoding); + mEncoder = &encoder; + + // Setup viewer + mViewer = new osgViewer::Viewer; + mViewer->setReleaseContextAtEndOfFrameHint(false); + mScreenCaptureOperation = new WriteScreenshotToFileOperation(mCfgMgr.getUserDataPath().string(), Settings::Manager::getString("screenshot format", "General")); @@ -788,10 +779,6 @@ void OMW::Engine::go() mEnvironment.setFrameRateLimit(Settings::Manager::getFloat("framerate limit", "Video")); - // Create encoder - ToUTF8::Utf8Encoder encoder (mEncoding); - mEncoder = &encoder; - prepareEngine (settings); /* @@ -814,6 +801,22 @@ void OMW::Engine::go() End of tes3mp change (major) */ + // Setup profiler + osg::ref_ptr statshandler = new Resource::Profiler; + + statshandler->addUserStatsLine("Script", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "script_time_taken", 1000.0, true, false, "script_time_begin", "script_time_end", 10000); + statshandler->addUserStatsLine("Mechanics", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "mechanics_time_taken", 1000.0, true, false, "mechanics_time_begin", "mechanics_time_end", 10000); + statshandler->addUserStatsLine("Physics", osg::Vec4f(1.f, 1.f, 1.f, 1.f), osg::Vec4f(1.f, 1.f, 1.f, 1.f), + "physics_time_taken", 1000.0, true, false, "physics_time_begin", "physics_time_end", 10000); + + mViewer->addEventHandler(statshandler); + + osg::ref_ptr resourceshandler = new Resource::StatsHandler; + mViewer->addEventHandler(resourceshandler); + + // Start the game if (!mSaveGameFile.empty()) { mEnvironment.getStateManager()->loadGame(mSaveGameFile); diff --git a/apps/openmw/main.cpp b/apps/openmw/main.cpp index d7bec6c85..132661b92 100644 --- a/apps/openmw/main.cpp +++ b/apps/openmw/main.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -34,18 +35,6 @@ #include #endif -#if (defined(__APPLE__) || (defined(__linux) && !defined(ANDROID)) || (defined(__unix) && !defined(ANDROID)) || defined(__posix)) - #define USE_CRASH_CATCHER 1 -#else - #define USE_CRASH_CATCHER 0 -#endif - -#if USE_CRASH_CATCHER -#include -extern int cc_install_handlers(int argc, char **argv, int num_signals, int *sigs, const char *logfile, int (*user_info)(char*, char*)); -extern int is_debugger_attached(void); -#endif - /* Start of tes3mp addition @@ -419,18 +408,7 @@ int main(int argc, char**argv) End of tes3mp addition */ - -#if USE_CRASH_CATCHER - // Unix crash catcher - if ((argc == 2 && strcmp(argv[1], "--cc-handle-crash") == 0) || !is_debugger_attached()) - { - int s[5] = { SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGABRT }; - cc_install_handlers(argc, argv, 5, s, (cfgMgr.getLogPath() / "crash.log").string().c_str(), NULL); - std::cout << "Installing crash catcher" << std::endl; - } - else - std::cout << "Running in a debugger, not installing crash catcher" << std::endl; -#endif + crashCatcherInstall(argc, argv, (cfgMgr.getLogPath() / "crash.log").string()); #ifdef __APPLE__ boost::filesystem::path binary_path = boost::filesystem::system_complete(boost::filesystem::path(argv[0])); diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 28fbccba6..ecf139d15 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -235,9 +235,12 @@ namespace MWBase /// Resurrects the player if necessary virtual void keepPlayerAlive() = 0; + virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const = 0; virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const = 0; virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const = 0; + virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0; + /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0; diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index 12f7fbb7a..d3506662e 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -157,7 +157,7 @@ namespace MWBase */ /// Make the player use an item, while updating GUI state accordingly - virtual void useItem(const MWWorld::Ptr& item) = 0; + virtual void useItem(const MWWorld::Ptr& item, bool force=false) = 0; virtual void updateSpellWindow() = 0; diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 7c2a612a3..115c6afe7 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -583,7 +583,7 @@ namespace MWBase */ virtual bool startSpellCast (const MWWorld::Ptr& actor) = 0; - virtual void castSpell (const MWWorld::Ptr& actor) = 0; + virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0; virtual void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) = 0; virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, diff --git a/apps/openmw/mwclass/apparatus.cpp b/apps/openmw/mwclass/apparatus.cpp index b4b219ef2..50e8ee302 100644 --- a/apps/openmw/mwclass/apparatus.cpp +++ b/apps/openmw/mwclass/apparatus.cpp @@ -157,10 +157,9 @@ namespace MWClass return info; } - - std::shared_ptr Apparatus::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Apparatus::use (const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionAlchemy()); + return std::shared_ptr(new MWWorld::ActionAlchemy(force)); } MWWorld::Ptr Apparatus::copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const diff --git a/apps/openmw/mwclass/apparatus.hpp b/apps/openmw/mwclass/apparatus.hpp index 539c1068b..e9b645763 100644 --- a/apps/openmw/mwclass/apparatus.hpp +++ b/apps/openmw/mwclass/apparatus.hpp @@ -50,8 +50,7 @@ namespace MWClass virtual std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const; ///< Return name of inventory icon. - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/armor.cpp b/apps/openmw/mwclass/armor.cpp index 2670804fe..b602205bf 100644 --- a/apps/openmw/mwclass/armor.cpp +++ b/apps/openmw/mwclass/armor.cpp @@ -387,9 +387,9 @@ namespace MWClass return std::make_pair(1,""); } - std::shared_ptr Armor::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Armor::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/armor.hpp b/apps/openmw/mwclass/armor.hpp index 628ac1b88..6b098fa9a 100644 --- a/apps/openmw/mwclass/armor.hpp +++ b/apps/openmw/mwclass/armor.hpp @@ -73,8 +73,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. \n /// Second item in the pair specifies the error message - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/book.cpp b/apps/openmw/mwclass/book.cpp index 0ab6579e1..49b37dad8 100644 --- a/apps/openmw/mwclass/book.cpp +++ b/apps/openmw/mwclass/book.cpp @@ -195,7 +195,7 @@ namespace MWClass return record->mId; } - std::shared_ptr Book::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Book::use (const MWWorld::Ptr& ptr, bool force) const { return std::shared_ptr(new MWWorld::ActionRead(ptr)); } diff --git a/apps/openmw/mwclass/book.hpp b/apps/openmw/mwclass/book.hpp index 82ca08a4d..4cb51ff58 100644 --- a/apps/openmw/mwclass/book.hpp +++ b/apps/openmw/mwclass/book.hpp @@ -53,7 +53,7 @@ namespace MWClass virtual std::string applyEnchantment(const MWWorld::ConstPtr &ptr, const std::string& enchId, int enchCharge, const std::string& newName) const; ///< Creates a new record using \a ptr as template, with the given name and the given enchantment applied to it. - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/clothing.cpp b/apps/openmw/mwclass/clothing.cpp index 3f08c3e78..b5f567859 100644 --- a/apps/openmw/mwclass/clothing.cpp +++ b/apps/openmw/mwclass/clothing.cpp @@ -276,9 +276,9 @@ namespace MWClass return std::make_pair (1, ""); } - std::shared_ptr Clothing::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Clothing::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/clothing.hpp b/apps/openmw/mwclass/clothing.hpp index 202b6596e..81b5c2ef4 100644 --- a/apps/openmw/mwclass/clothing.hpp +++ b/apps/openmw/mwclass/clothing.hpp @@ -65,8 +65,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/ingredient.cpp b/apps/openmw/mwclass/ingredient.cpp index af33b4f7a..d29ed3845 100644 --- a/apps/openmw/mwclass/ingredient.cpp +++ b/apps/openmw/mwclass/ingredient.cpp @@ -108,7 +108,7 @@ namespace MWClass } - std::shared_ptr Ingredient::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Ingredient::use (const MWWorld::Ptr& ptr, bool force) const { std::shared_ptr action (new MWWorld::ActionEat (ptr)); diff --git a/apps/openmw/mwclass/ingredient.hpp b/apps/openmw/mwclass/ingredient.hpp index 1136eb50f..668fea29a 100644 --- a/apps/openmw/mwclass/ingredient.hpp +++ b/apps/openmw/mwclass/ingredient.hpp @@ -36,8 +36,7 @@ namespace MWClass virtual int getValue (const MWWorld::ConstPtr& ptr) const; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu static void registerSelf(); diff --git a/apps/openmw/mwclass/light.cpp b/apps/openmw/mwclass/light.cpp index aeeb89a72..27a9f3f2b 100644 --- a/apps/openmw/mwclass/light.cpp +++ b/apps/openmw/mwclass/light.cpp @@ -186,9 +186,9 @@ namespace MWClass return Class::showsInInventory(ptr); } - std::shared_ptr Light::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Light::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/light.hpp b/apps/openmw/mwclass/light.hpp index 59085d97e..013b9eee3 100644 --- a/apps/openmw/mwclass/light.hpp +++ b/apps/openmw/mwclass/light.hpp @@ -55,8 +55,7 @@ namespace MWClass virtual std::string getInventoryIcon (const MWWorld::ConstPtr& ptr) const; ///< Return name of inventory icon. - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual void setRemainingUsageTime (const MWWorld::Ptr& ptr, float duration) const; diff --git a/apps/openmw/mwclass/lockpick.cpp b/apps/openmw/mwclass/lockpick.cpp index 15499c1a8..5673465a0 100644 --- a/apps/openmw/mwclass/lockpick.cpp +++ b/apps/openmw/mwclass/lockpick.cpp @@ -140,9 +140,9 @@ namespace MWClass return info; } - std::shared_ptr Lockpick::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Lockpick::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/lockpick.hpp b/apps/openmw/mwclass/lockpick.hpp index 1bcf7fb85..c8df860f6 100644 --- a/apps/openmw/mwclass/lockpick.hpp +++ b/apps/openmw/mwclass/lockpick.hpp @@ -53,8 +53,7 @@ namespace MWClass virtual std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const; - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/misc.cpp b/apps/openmw/mwclass/misc.cpp index 2f156c822..73e5f4f3c 100644 --- a/apps/openmw/mwclass/misc.cpp +++ b/apps/openmw/mwclass/misc.cpp @@ -259,7 +259,7 @@ namespace MWClass return newPtr; } - std::shared_ptr Miscellaneous::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Miscellaneous::use (const MWWorld::Ptr& ptr, bool force) const { if (ptr.getCellRef().getSoul().empty() || !MWBase::Environment::get().getWorld()->getStore().get().search(ptr.getCellRef().getSoul())) return std::shared_ptr(new MWWorld::NullAction()); diff --git a/apps/openmw/mwclass/misc.hpp b/apps/openmw/mwclass/misc.hpp index 37e77860b..77e7d105b 100644 --- a/apps/openmw/mwclass/misc.hpp +++ b/apps/openmw/mwclass/misc.hpp @@ -49,8 +49,7 @@ namespace MWClass virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual float getWeight (const MWWorld::ConstPtr& ptr) const; diff --git a/apps/openmw/mwclass/potion.cpp b/apps/openmw/mwclass/potion.cpp index 2b41b7dbb..7da9fb1ce 100644 --- a/apps/openmw/mwclass/potion.cpp +++ b/apps/openmw/mwclass/potion.cpp @@ -172,7 +172,7 @@ namespace MWClass return info; } - std::shared_ptr Potion::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Potion::use (const MWWorld::Ptr& ptr, bool force) const { MWWorld::LiveCellRef *ref = ptr.get(); diff --git a/apps/openmw/mwclass/potion.hpp b/apps/openmw/mwclass/potion.hpp index ad9a329c0..90dbe5281 100644 --- a/apps/openmw/mwclass/potion.hpp +++ b/apps/openmw/mwclass/potion.hpp @@ -36,7 +36,7 @@ namespace MWClass virtual int getValue (const MWWorld::ConstPtr& ptr) const; ///< Return trade value of the object. Throws an exception, if the object can't be traded. - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu static void registerSelf(); diff --git a/apps/openmw/mwclass/probe.cpp b/apps/openmw/mwclass/probe.cpp index 030ee3f8b..734b6b1fd 100644 --- a/apps/openmw/mwclass/probe.cpp +++ b/apps/openmw/mwclass/probe.cpp @@ -140,9 +140,9 @@ namespace MWClass return info; } - std::shared_ptr Probe::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Probe::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/probe.hpp b/apps/openmw/mwclass/probe.hpp index 9ac3ab0c9..86f6f677a 100644 --- a/apps/openmw/mwclass/probe.hpp +++ b/apps/openmw/mwclass/probe.hpp @@ -53,8 +53,7 @@ namespace MWClass virtual std::pair canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const; - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwclass/repair.cpp b/apps/openmw/mwclass/repair.cpp index c1a832a59..60c48ca05 100644 --- a/apps/openmw/mwclass/repair.cpp +++ b/apps/openmw/mwclass/repair.cpp @@ -182,9 +182,9 @@ namespace MWClass return MWWorld::Ptr(cell.insert(ref), &cell); } - std::shared_ptr Repair::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Repair::use (const MWWorld::Ptr& ptr, bool force) const { - return std::shared_ptr(new MWWorld::ActionRepair(ptr)); + return std::shared_ptr(new MWWorld::ActionRepair(ptr, force)); } bool Repair::canSell (const MWWorld::ConstPtr& item, int npcServices) const diff --git a/apps/openmw/mwclass/repair.hpp b/apps/openmw/mwclass/repair.hpp index 34ba4035b..f60dbad90 100644 --- a/apps/openmw/mwclass/repair.hpp +++ b/apps/openmw/mwclass/repair.hpp @@ -49,7 +49,7 @@ namespace MWClass virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu (default implementation: return a /// null action). diff --git a/apps/openmw/mwclass/weapon.cpp b/apps/openmw/mwclass/weapon.cpp index 1b4c46666..1cda13993 100644 --- a/apps/openmw/mwclass/weapon.cpp +++ b/apps/openmw/mwclass/weapon.cpp @@ -435,9 +435,9 @@ namespace MWClass return std::make_pair(1, ""); } - std::shared_ptr Weapon::use (const MWWorld::Ptr& ptr) const + std::shared_ptr Weapon::use (const MWWorld::Ptr& ptr, bool force) const { - std::shared_ptr action(new MWWorld::ActionEquip(ptr)); + std::shared_ptr action(new MWWorld::ActionEquip(ptr, force)); action->setSound(getUpSoundId(ptr)); diff --git a/apps/openmw/mwclass/weapon.hpp b/apps/openmw/mwclass/weapon.hpp index c14847363..423ba0dd2 100644 --- a/apps/openmw/mwclass/weapon.hpp +++ b/apps/openmw/mwclass/weapon.hpp @@ -72,8 +72,7 @@ namespace MWClass ///< Return 0 if player cannot equip item. 1 if can equip. 2 if it's twohanded weapon. 3 if twohanded weapon conflicts with that. /// Second item in the pair specifies the error message - virtual std::shared_ptr use (const MWWorld::Ptr& ptr) - const; + virtual std::shared_ptr use (const MWWorld::Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu virtual std::string getModel(const MWWorld::ConstPtr &ptr) const; diff --git a/apps/openmw/mwgui/dialogue.cpp b/apps/openmw/mwgui/dialogue.cpp index 450799f29..f4fe54917 100644 --- a/apps/openmw/mwgui/dialogue.cpp +++ b/apps/openmw/mwgui/dialogue.cpp @@ -629,6 +629,9 @@ namespace MWGui void DialogueWindow::onTopicActivated(const std::string &topicId) { + if (mGoodbye) + return; + MWBase::Environment::get().getDialogueManager()->keywordSelected(topicId, mCallback.get()); updateTopics(); } diff --git a/apps/openmw/mwgui/inventorywindow.cpp b/apps/openmw/mwgui/inventorywindow.cpp index 0ff317e09..66bbec76a 100644 --- a/apps/openmw/mwgui/inventorywindow.cpp +++ b/apps/openmw/mwgui/inventorywindow.cpp @@ -39,7 +39,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwworld/class.hpp" -#include "../mwworld/action.hpp" +#include "../mwworld/actionequip.hpp" #include "../mwscript/interpretercontext.hpp" #include "../mwmechanics/actorutil.hpp" @@ -488,7 +488,7 @@ namespace MWGui MWBase::Environment::get().getWindowManager()->toggleVisible(GW_Inventory); } - void InventoryWindow::useItem(const MWWorld::Ptr &ptr) + void InventoryWindow::useItem(const MWWorld::Ptr &ptr, bool force) { const std::string& script = ptr.getClass().getScript(ptr); @@ -497,13 +497,24 @@ namespace MWGui // early-out for items that need to be equipped, but can't be equipped: we don't want to set OnPcEquip in that case if (!ptr.getClass().getEquipmentSlots(ptr).first.empty()) { - std::pair canEquip = ptr.getClass().canBeEquipped(ptr, player); - if (canEquip.first == 0) + if (ptr.getClass().hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0) { - MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second); + MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); updateItemView(); return; } + + if (!force) + { + std::pair canEquip = ptr.getClass().canBeEquipped(ptr, player); + + if (canEquip.first == 0) + { + MWBase::Environment::get().getWindowManager()->messageBox(canEquip.second); + updateItemView(); + return; + } + } } // If the item has a script, set its OnPcEquip to 1 @@ -526,9 +537,8 @@ namespace MWGui { if (script.empty() || ptr.getRefData().getLocals().getIntVar(script, "pcskipequip") == 0) { - std::shared_ptr action = ptr.getClass().use(ptr); - - action->execute (player); + std::shared_ptr action = ptr.getClass().use(ptr, force); + action->execute(player); } else mSkippedToEquip = ptr; diff --git a/apps/openmw/mwgui/inventorywindow.hpp b/apps/openmw/mwgui/inventorywindow.hpp index 5576b52ed..124fe7b0e 100644 --- a/apps/openmw/mwgui/inventorywindow.hpp +++ b/apps/openmw/mwgui/inventorywindow.hpp @@ -58,7 +58,7 @@ namespace MWGui void clear(); - void useItem(const MWWorld::Ptr& ptr); + void useItem(const MWWorld::Ptr& ptr, bool force=false); void setGuiMode(GuiMode mode); diff --git a/apps/openmw/mwgui/trainingwindow.cpp b/apps/openmw/mwgui/trainingwindow.cpp index e23c3acf6..6dafcac7c 100644 --- a/apps/openmw/mwgui/trainingwindow.cpp +++ b/apps/openmw/mwgui/trainingwindow.cpp @@ -91,7 +91,7 @@ namespace MWGui for (int i=0; iuseItem(item); + mInventoryWindow->useItem(item, bypassBeastRestrictions); } bool WindowManager::isAllowed (GuiWindow wnd) const diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index d4862c726..737747668 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -196,7 +196,7 @@ namespace MWGui */ /// Make the player use an item, while updating GUI state accordingly - virtual void useItem(const MWWorld::Ptr& item); + virtual void useItem(const MWWorld::Ptr& item, bool bypassBeastRestrictions=false); virtual void updateSpellWindow(); diff --git a/apps/openmw/mwmechanics/actor.cpp b/apps/openmw/mwmechanics/actor.cpp index 675bd160a..f7c6b7f1c 100644 --- a/apps/openmw/mwmechanics/actor.cpp +++ b/apps/openmw/mwmechanics/actor.cpp @@ -4,7 +4,6 @@ namespace MWMechanics { - Actor::Actor(const MWWorld::Ptr &ptr, MWRender::Animation *animation) { mCharacterController.reset(new CharacterController(ptr, animation)); @@ -19,10 +18,4 @@ namespace MWMechanics { return mCharacterController.get(); } - - AiState& Actor::getAiState() - { - return mAiState; - } - } diff --git a/apps/openmw/mwmechanics/actor.hpp b/apps/openmw/mwmechanics/actor.hpp index b8e114ead..119527b64 100644 --- a/apps/openmw/mwmechanics/actor.hpp +++ b/apps/openmw/mwmechanics/actor.hpp @@ -3,8 +3,6 @@ #include -#include "aistate.hpp" - namespace MWRender { class Animation; @@ -29,12 +27,8 @@ namespace MWMechanics CharacterController* getCharacterController(); - AiState& getAiState(); - private: std::unique_ptr mCharacterController; - - AiState mAiState; }; } diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index b42e78df1..c37965ee6 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -1253,6 +1253,13 @@ namespace MWMechanics } } + void Actors::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + { + PtrActorMap::iterator iter = mActors.find(ptr); + if(iter != mActors.end()) + iter->second->getCharacterController()->castSpell(spellId, manualSpell); + } + bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) { if (!actor.getClass().isActor()) @@ -1523,7 +1530,7 @@ namespace MWMechanics { CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first); if (isConscious(iter->first)) - stats.getAiSequence().execute(iter->first, *iter->second->getCharacterController(), iter->second->getAiState(), duration); + stats.getAiSequence().execute(iter->first, *iter->second->getCharacterController(), duration); } } /* @@ -2195,6 +2202,15 @@ namespace MWMechanics return it->second->getCharacterController()->isReadyToBlock(); } + bool Actors::isCastingSpell(const MWWorld::Ptr &ptr) const + { + PtrActorMap::const_iterator it = mActors.find(ptr); + if (it == mActors.end()) + return false; + + return it->second->getCharacterController()->isCastingSpell(); + } + bool Actors::isAttackingOrSpell(const MWWorld::Ptr& ptr) const { PtrActorMap::const_iterator it = mActors.find(ptr); @@ -2220,7 +2236,7 @@ namespace MWMechanics || ptr.getClass().getCreatureStats(ptr).isParalyzed()) continue; MWMechanics::AiSequence& seq = ptr.getClass().getCreatureStats(ptr).getAiSequence(); - seq.fastForward(ptr, it->second->getAiState()); + seq.fastForward(ptr); } } } diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 4d35e9c47..46c6d3056 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -77,6 +77,8 @@ namespace MWMechanics /// /// \note Ignored, if \a ptr is not a registered actor. + void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + void updateActor(const MWWorld::Ptr &old, const MWWorld::Ptr& ptr); ///< Updates an actor with a new Ptr @@ -171,6 +173,7 @@ namespace MWMechanics void clear(); // Clear death counter + bool isCastingSpell(const MWWorld::Ptr& ptr) const; bool isReadyToBlock(const MWWorld::Ptr& ptr) const; bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const; diff --git a/apps/openmw/mwmechanics/aicast.cpp b/apps/openmw/mwmechanics/aicast.cpp new file mode 100644 index 000000000..48cb17f6d --- /dev/null +++ b/apps/openmw/mwmechanics/aicast.cpp @@ -0,0 +1,84 @@ +#include "aicast.hpp" + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" + +#include "../mwworld/class.hpp" + +#include "aicombataction.hpp" +#include "creaturestats.hpp" +#include "spellcasting.hpp" +#include "steering.hpp" + +MWMechanics::AiCast::AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell) + : mTargetId(targetId), mSpellId(spellId), mCasting(false), mManual(manualSpell), mDistance(0) +{ + ActionSpell action = ActionSpell(spellId); + bool isRanged; + mDistance = action.getCombatRange(isRanged); +} + +MWMechanics::AiPackage *MWMechanics::AiCast::clone() const +{ + return new AiCast(*this); +} + +bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::CharacterController& characterController, MWMechanics::AiState& state, float duration) +{ + MWWorld::Ptr target; + if (actor.getCellRef().getRefId() == mTargetId) + { + // If the target has the same ID as caster, consider that actor casts spell with Self range. + target = actor; + } + else + { + target = getTarget(); + if (!target) + return true; + + if (!mManual && !pathTo(actor, target.getRefData().getPosition().pos, duration, mDistance)) + { + return false; + } + + osg::Vec3f dir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3(); + bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f)); + turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); + + if (!turned) + return false; + } + + // Check if the actor is already casting another spell + bool isCasting = MWBase::Environment::get().getMechanicsManager()->isCastingSpell(actor); + if (isCasting && !mCasting) + return false; + + if (!mCasting) + { + MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual); + mCasting = true; + return false; + } + + // Finish package, if actor finished spellcasting + return !isCasting; +} + +MWWorld::Ptr MWMechanics::AiCast::getTarget() const +{ + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mTargetId, false); + + return target; +} + +int MWMechanics::AiCast::getTypeId() const +{ + return AiPackage::TypeIdCast; +} + +unsigned int MWMechanics::AiCast::getPriority() const +{ + return 3; +} diff --git a/apps/openmw/mwmechanics/aicast.hpp b/apps/openmw/mwmechanics/aicast.hpp new file mode 100644 index 000000000..70a8a6bbd --- /dev/null +++ b/apps/openmw/mwmechanics/aicast.hpp @@ -0,0 +1,37 @@ +#ifndef GAME_MWMECHANICS_AICAST_H +#define GAME_MWMECHANICS_AICAST_H + +#include "../mwbase/world.hpp" + +#include "aipackage.hpp" + +namespace MWMechanics +{ + /// AiPackage which makes an actor to cast given spell. + class AiCast : public AiPackage { + public: + AiCast(const std::string& targetId, const std::string& spellId, bool manualSpell=false); + + virtual AiPackage *clone() const; + + virtual bool execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration); + + virtual int getTypeId() const; + + virtual MWWorld::Ptr getTarget() const; + + virtual unsigned int getPriority() const; + + virtual bool canCancel() const { return false; } + virtual bool shouldCancelPreviousAi() const { return false; } + + private: + std::string mTargetId; + std::string mSpellId; + bool mCasting; + bool mManual; + float mDistance; + }; +} + +#endif diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index f96e08476..3150d6b30 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -48,74 +48,6 @@ namespace namespace MWMechanics { - - /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. - struct AiCombatStorage : AiTemporaryBase - { - float mAttackCooldown; - float mTimerReact; - float mTimerCombatMove; - bool mReadyToAttack; - bool mAttack; - float mAttackRange; - bool mCombatMove; - osg::Vec3f mLastTargetPos; - const MWWorld::CellStore* mCell; - std::shared_ptr mCurrentAction; - float mActionCooldown; - float mStrength; - bool mForceNoShortcut; - ESM::Position mShortcutFailPos; - MWMechanics::Movement mMovement; - - enum FleeState - { - FleeState_None, - FleeState_Idle, - FleeState_RunBlindly, - FleeState_RunToDestination - }; - FleeState mFleeState; - bool mLOS; - float mUpdateLOSTimer; - float mFleeBlindRunTimer; - ESM::Pathgrid::Point mFleeDest; - - AiCombatStorage(): - mAttackCooldown(0.0f), - mTimerReact(AI_REACTION_TIME), - mTimerCombatMove(0.0f), - mReadyToAttack(false), - mAttack(false), - mAttackRange(0.0f), - mCombatMove(false), - mLastTargetPos(0,0,0), - mCell(NULL), - mCurrentAction(), - mActionCooldown(0.0f), - mStrength(), - mForceNoShortcut(false), - mShortcutFailPos(), - mMovement(), - mFleeState(FleeState_None), - mLOS(false), - mUpdateLOSTimer(0.0f), - mFleeBlindRunTimer(0.0f) - {} - - void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); - void updateCombatMove(float duration); - void stopCombatMove(); - void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, - const ESM::Weapon* weapon, bool distantCombat); - void updateAttack(CharacterController& characterController); - void stopAttack(); - - void startFleeing(); - void stopFleeing(); - bool isFleeing(); - }; - AiCombat::AiCombat(const MWWorld::Ptr& actor) { mTargetActorId = actor.getClass().getCreatureStats(actor).getActorId(); @@ -128,7 +60,7 @@ namespace MWMechanics void AiCombat::init() { - + } /* diff --git a/apps/openmw/mwmechanics/aicombat.hpp b/apps/openmw/mwmechanics/aicombat.hpp index 6e1f0c623..7c9891bcc 100644 --- a/apps/openmw/mwmechanics/aicombat.hpp +++ b/apps/openmw/mwmechanics/aicombat.hpp @@ -23,7 +23,72 @@ namespace MWMechanics { class Action; - struct AiCombatStorage; + /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. + struct AiCombatStorage : AiTemporaryBase + { + float mAttackCooldown; + float mTimerReact; + float mTimerCombatMove; + bool mReadyToAttack; + bool mAttack; + float mAttackRange; + bool mCombatMove; + osg::Vec3f mLastTargetPos; + const MWWorld::CellStore* mCell; + std::shared_ptr mCurrentAction; + float mActionCooldown; + float mStrength; + bool mForceNoShortcut; + ESM::Position mShortcutFailPos; + MWMechanics::Movement mMovement; + + enum FleeState + { + FleeState_None, + FleeState_Idle, + FleeState_RunBlindly, + FleeState_RunToDestination + }; + FleeState mFleeState; + bool mLOS; + float mUpdateLOSTimer; + float mFleeBlindRunTimer; + ESM::Pathgrid::Point mFleeDest; + + AiCombatStorage(): + mAttackCooldown(0.0f), + mTimerReact(AI_REACTION_TIME), + mTimerCombatMove(0.0f), + mReadyToAttack(false), + mAttack(false), + mAttackRange(0.0f), + mCombatMove(false), + mLastTargetPos(0,0,0), + mCell(NULL), + mCurrentAction(), + mActionCooldown(0.0f), + mStrength(), + mForceNoShortcut(false), + mShortcutFailPos(), + mMovement(), + mFleeState(FleeState_None), + mLOS(false), + mUpdateLOSTimer(0.0f), + mFleeBlindRunTimer(0.0f) + {} + + void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); + void updateCombatMove(float duration); + void stopCombatMove(); + void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, + const ESM::Weapon* weapon, bool distantCombat); + void updateAttack(CharacterController& characterController); + void stopAttack(); + + void startFleeing(); + void stopFleeing(); + bool isFleeing(); + }; /// \brief Causes the actor to fight another actor class AiCombat : public AiPackage diff --git a/apps/openmw/mwmechanics/aifollow.cpp b/apps/openmw/mwmechanics/aifollow.cpp index b40f15618..f6d6ee2f8 100644 --- a/apps/openmw/mwmechanics/aifollow.cpp +++ b/apps/openmw/mwmechanics/aifollow.cpp @@ -17,22 +17,6 @@ namespace MWMechanics { - -struct AiFollowStorage : AiTemporaryBase -{ - float mTimer; - bool mMoving; - float mTargetAngleRadians; - bool mTurnActorToTarget; - - AiFollowStorage() : - mTimer(0.f), - mMoving(false), - mTargetAngleRadians(0.f), - mTurnActorToTarget(false) - {} -}; - int AiFollow::mFollowIndexCounter = 0; AiFollow::AiFollow(const std::string &actorId, float duration, float x, float y, float z) diff --git a/apps/openmw/mwmechanics/aifollow.hpp b/apps/openmw/mwmechanics/aifollow.hpp index 66d334d32..5a9352ecd 100644 --- a/apps/openmw/mwmechanics/aifollow.hpp +++ b/apps/openmw/mwmechanics/aifollow.hpp @@ -19,6 +19,21 @@ namespace AiSequence namespace MWMechanics { + struct AiFollowStorage : AiTemporaryBase + { + float mTimer; + bool mMoving; + float mTargetAngleRadians; + bool mTurnActorToTarget; + + AiFollowStorage() : + mTimer(0.f), + mMoving(false), + mTargetAngleRadians(0.f), + mTurnActorToTarget(false) + {} + }; + /// \brief AiPackage for an actor to follow another actor/the PC /** The AI will follow the target until a condition (time, or position) are set. Both can be disabled to cause the actor to follow the other indefinitely **/ diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index 2b685accc..f1941ff1d 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -49,7 +49,8 @@ namespace MWMechanics TypeIdAvoidDoor = 7, TypeIdFace = 8, TypeIdBreathe = 9, - TypeIdInternalTravel = 10 + TypeIdInternalTravel = 10, + TypeIdCast = 11 }; ///Default constructor diff --git a/apps/openmw/mwmechanics/aisequence.cpp b/apps/openmw/mwmechanics/aisequence.cpp index 85afbc453..085174820 100644 --- a/apps/openmw/mwmechanics/aisequence.cpp +++ b/apps/openmw/mwmechanics/aisequence.cpp @@ -180,15 +180,11 @@ bool AiSequence::isPackageDone() const bool isActualAiPackage(int packageTypeId) { - return (packageTypeId != AiPackage::TypeIdCombat - && packageTypeId != AiPackage::TypeIdPursue - && packageTypeId != AiPackage::TypeIdAvoidDoor - && packageTypeId != AiPackage::TypeIdFace - && packageTypeId != AiPackage::TypeIdBreathe - && packageTypeId != AiPackage::TypeIdInternalTravel); + return (packageTypeId >= AiPackage::TypeIdWander && + packageTypeId <= AiPackage::TypeIdActivate); } -void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) +void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration) { if(actor != getPlayer()) { @@ -262,7 +258,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac try { - if (package->execute (actor,characterController,state,duration)) + if (package->execute (actor, characterController, mAiState, duration)) { // Put repeating noncombat AI packages on the end of the stack so they can be used again if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) @@ -308,7 +304,7 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (isActualAiPackage(package.getTypeId())) stopCombat(); - // We should return a wandering actor back after combat or pursuit. + // We should return a wandering actor back after combat, casting or pursuit. // The same thing for actors without AI packages. // Also there is no point to stack return packages. int currentTypeId = getTypeId(); @@ -316,7 +312,8 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo if (currentTypeId <= MWMechanics::AiPackage::TypeIdWander && !hasPackage(MWMechanics::AiPackage::TypeIdInternalTravel) && (newTypeId <= MWMechanics::AiPackage::TypeIdCombat - || newTypeId == MWMechanics::AiPackage::TypeIdPursue)) + || newTypeId == MWMechanics::AiPackage::TypeIdPursue + || newTypeId == MWMechanics::AiPackage::TypeIdCast)) { osg::Vec3f dest; if (currentTypeId == MWMechanics::AiPackage::TypeIdWander) @@ -352,6 +349,13 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo // insert new package in correct place depending on priority for(std::list::iterator it = mPackages.begin(); it != mPackages.end(); ++it) { + // We should keep current AiCast package, if we try to add a new one. + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdCast && + package.getTypeId() == MWMechanics::AiPackage::TypeIdCast) + { + continue; + } + if((*it)->getPriority() <= package.getPriority()) { mPackages.insert(it,package.clone()); @@ -360,6 +364,14 @@ void AiSequence::stack (const AiPackage& package, const MWWorld::Ptr& actor, boo } mPackages.push_back (package.clone()); + + // Make sure that temporary storage is empty + if (cancelOther) + { + mAiState.moveIn(new AiCombatStorage()); + mAiState.moveIn(new AiFollowStorage()); + mAiState.moveIn(new AiWanderStorage()); + } } AiPackage* MWMechanics::AiSequence::getActivePackage() @@ -494,12 +506,12 @@ void AiSequence::readState(const ESM::AiSequence::AiSequence &sequence) mLastAiPackage = sequence.mLastAiPackage; } -void AiSequence::fastForward(const MWWorld::Ptr& actor, AiState& state) +void AiSequence::fastForward(const MWWorld::Ptr& actor) { if (!mPackages.empty()) { MWMechanics::AiPackage* package = mPackages.front(); - package->fastForward(actor, state); + package->fastForward(actor, mAiState); } } diff --git a/apps/openmw/mwmechanics/aisequence.hpp b/apps/openmw/mwmechanics/aisequence.hpp index d725409de..5c72bcc4c 100644 --- a/apps/openmw/mwmechanics/aisequence.hpp +++ b/apps/openmw/mwmechanics/aisequence.hpp @@ -3,6 +3,8 @@ #include +#include "aistate.hpp" + #include namespace MWWorld @@ -47,6 +49,7 @@ namespace MWMechanics /// The type of AI package that ran last int mLastAiPackage; + AiState mAiState; public: ///Default constructor @@ -104,10 +107,10 @@ namespace MWMechanics void stopPursuit(); /// Execute current package, switching if needed. - void execute (const MWWorld::Ptr& actor, CharacterController& characterController, MWMechanics::AiState& state, float duration); + void execute (const MWWorld::Ptr& actor, CharacterController& characterController, float duration); /// Simulate the passing of time using the currently active AI package - void fastForward(const MWWorld::Ptr &actor, AiState &state); + void fastForward(const MWWorld::Ptr &actor); /// Remove all packages. void clear(); diff --git a/apps/openmw/mwmechanics/aiwander.cpp b/apps/openmw/mwmechanics/aiwander.cpp index ee680159e..928b09cf9 100644 --- a/apps/openmw/mwmechanics/aiwander.cpp +++ b/apps/openmw/mwmechanics/aiwander.cpp @@ -51,67 +51,6 @@ namespace MWMechanics std::string("idle9"), }; - /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. - struct AiWanderStorage : AiTemporaryBase - { - // the z rotation angle to reach - // when mTurnActorGivingGreetingToFacePlayer is true - float mTargetAngleRadians; - bool mTurnActorGivingGreetingToFacePlayer; - float mReaction; // update some actions infrequently - - AiWander::GreetingState mSaidGreeting; - int mGreetingTimer; - - const MWWorld::CellStore* mCell; // for detecting cell change - - // AiWander states - AiWander::WanderState mState; - - bool mIsWanderingManually; - bool mCanWanderAlongPathGrid; - - unsigned short mIdleAnimation; - std::vector mBadIdles; // Idle animations that when called cause errors - - // do we need to calculate allowed nodes based on mDistance - bool mPopulateAvailableNodes; - - // allowed pathgrid nodes based on mDistance from the spawn point - // in local coordinates of mCell - std::vector mAllowedNodes; - - ESM::Pathgrid::Point mCurrentNode; - bool mTrimCurrentNode; - - float mDoorCheckDuration; - int mStuckCount; - - AiWanderStorage(): - mTargetAngleRadians(0), - mTurnActorGivingGreetingToFacePlayer(false), - mReaction(0), - mSaidGreeting(AiWander::Greet_None), - mGreetingTimer(0), - mCell(NULL), - mState(AiWander::Wander_ChooseAction), - mIsWanderingManually(false), - mCanWanderAlongPathGrid(true), - mIdleAnimation(0), - mBadIdles(), - mPopulateAvailableNodes(true), - mAllowedNodes(), - mTrimCurrentNode(false), - mDoorCheckDuration(0), // TODO: maybe no longer needed - mStuckCount(0) - {}; - - void setState(const AiWander::WanderState wanderState, const bool isManualWander = false) { - mState = wanderState; - mIsWanderingManually = isManualWander; - } - }; - AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mRemainingDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat), mStoredInitialActorPosition(false), mInitialActorPosition(osg::Vec3f(0, 0, 0)), mHasDestination(false), mDestination(osg::Vec3f(0, 0, 0)) @@ -221,7 +160,7 @@ namespace MWMechanics mPathFinder.buildSyncedPath(start, dest, actor.getCell(), getPathGridGraph(actor.getCell())); if (mPathFinder.isPathConstructed()) - storage.setState(Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking); } doPerFrameActionsForState(actor, duration, storage, pos); @@ -270,7 +209,7 @@ namespace MWMechanics if(actorCanMoveByZ && mDistance > 0) { // Typically want to idle for a short time before the next wander - if (Misc::Rng::rollDice(100) >= 92 && storage.mState != Wander_Walking) { + if (Misc::Rng::rollDice(100) >= 92 && storage.mState != AiWanderStorage::Wander_Walking) { wanderNearStart(actor, storage, mDistance); } @@ -283,7 +222,7 @@ namespace MWMechanics if (Misc::Rng::rollDice(100) >= 96) { wanderNearStart(actor, storage, mDistance); } else { - storage.setState(Wander_IdleNow); + storage.setState(AiWanderStorage::Wander_IdleNow); } } else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) { storage.mCanWanderAlongPathGrid = false; @@ -299,13 +238,13 @@ namespace MWMechanics mDistance = 0; // Allow interrupting a walking actor to trigger a greeting - WanderState& wanderState = storage.mState; - if ((wanderState == Wander_IdleNow) || (wanderState == Wander_Walking)) + AiWanderStorage::WanderState& wanderState = storage.mState; + if ((wanderState == AiWanderStorage::Wander_IdleNow) || (wanderState == AiWanderStorage::Wander_Walking)) { playGreetingIfPlayerGetsTooClose(actor, storage); } - if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid) + if ((wanderState == AiWanderStorage::Wander_MoveNow) && storage.mCanWanderAlongPathGrid) { // Construct a new path if there isn't one if(!mPathFinder.isPathConstructed()) @@ -381,7 +320,7 @@ namespace MWMechanics if (mPathFinder.isPathConstructed()) { - storage.setState(Wander_Walking, true); + storage.setState(AiWanderStorage::Wander_Walking, true); mHasDestination = true; } return; @@ -410,26 +349,26 @@ namespace MWMechanics void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { stopWalking(actor, storage); mObstacleCheck.clear(); - storage.setState(Wander_IdleNow); + storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage, ESM::Position& pos) { switch (storage.mState) { - case Wander_IdleNow: + case AiWanderStorage::Wander_IdleNow: onIdleStatePerFrameActions(actor, duration, storage); break; - case Wander_Walking: + case AiWanderStorage::Wander_Walking: onWalkingStatePerFrameActions(actor, duration, storage, pos); break; - case Wander_ChooseAction: + case AiWanderStorage::Wander_ChooseAction: onChooseActionStatePerFrameActions(actor, storage); break; - case Wander_MoveNow: + case AiWanderStorage::Wander_MoveNow: break; // nothing to do default: @@ -451,7 +390,7 @@ namespace MWMechanics if (mDistance && // actor is not intended to be stationary proximityToDoor(actor, distance*1.6f)) { - storage.setState(Wander_MoveNow); + storage.setState(AiWanderStorage::Wander_MoveNow); storage.mTrimCurrentNode = false; // just in case return; } @@ -468,13 +407,13 @@ namespace MWMechanics } // Check if idle animation finished - GreetingState& greetingState = storage.mSaidGreeting; - if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == Greet_Done || greetingState == Greet_None)) + AiWanderStorage::GreetingState& greetingState = storage.mSaidGreeting; + if (!checkIdle(actor, storage.mIdleAnimation) && (greetingState == AiWanderStorage::Greet_Done || greetingState == AiWanderStorage::Greet_None)) { if (mPathFinder.isPathConstructed()) - storage.setState(Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking); else - storage.setState(Wander_ChooseAction); + storage.setState(AiWanderStorage::Wander_ChooseAction); } } @@ -485,7 +424,7 @@ namespace MWMechanics if ((!mPathFinder.isPathConstructed()) || pathTo(actor, ESM::Pathgrid::Point(mPathFinder.getPath().back()), duration, DESTINATION_TOLERANCE)) { stopWalking(actor, storage); - storage.setState(Wander_ChooseAction); + storage.setState(AiWanderStorage::Wander_ChooseAction); } else { @@ -502,7 +441,7 @@ namespace MWMechanics if (!idleAnimation && mDistance) { - storage.setState(Wander_MoveNow); + storage.setState(AiWanderStorage::Wander_MoveNow); return; } if(idleAnimation) @@ -512,13 +451,13 @@ namespace MWMechanics if(!playIdle(actor, idleAnimation)) { storage.mBadIdles.push_back(idleAnimation); - storage.setState(Wander_ChooseAction); + storage.setState(AiWanderStorage::Wander_ChooseAction); return; } } } - storage.setState(Wander_IdleNow); + storage.setState(AiWanderStorage::Wander_IdleNow); } void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos) @@ -534,7 +473,7 @@ namespace MWMechanics trimAllowedNodes(storage.mAllowedNodes, mPathFinder); mObstacleCheck.clear(); stopWalking(actor, storage); - storage.setState(Wander_MoveNow); + storage.setState(AiWanderStorage::Wander_MoveNow); } storage.mStuckCount++; // TODO: maybe no longer needed @@ -545,7 +484,7 @@ namespace MWMechanics { mObstacleCheck.clear(); stopWalking(actor, storage); - storage.setState(Wander_ChooseAction); + storage.setState(AiWanderStorage::Wander_ChooseAction); storage.mStuckCount = 0; } } @@ -596,8 +535,8 @@ namespace MWMechanics float playerDistSqr = (playerPos - actorPos).length2(); int& greetingTimer = storage.mGreetingTimer; - GreetingState& greetingState = storage.mSaidGreeting; - if (greetingState == Greet_None) + AiWanderStorage::GreetingState& greetingState = storage.mSaidGreeting; + if (greetingState == AiWanderStorage::Greet_None) { if ((playerDistSqr <= helloDistance*helloDistance) && !player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getWorld()->getLOS(player, actor) @@ -606,37 +545,37 @@ namespace MWMechanics if (greetingTimer >= GREETING_SHOULD_START) { - greetingState = Greet_InProgress; + greetingState = AiWanderStorage::Greet_InProgress; MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); greetingTimer = 0; } } - if (greetingState == Greet_InProgress) + if (greetingState == AiWanderStorage::Greet_InProgress) { greetingTimer++; - if (storage.mState == Wander_Walking) + if (storage.mState == AiWanderStorage::Wander_Walking) { stopWalking(actor, storage, false); mObstacleCheck.clear(); - storage.setState(Wander_IdleNow); + storage.setState(AiWanderStorage::Wander_IdleNow); } turnActorToFacePlayer(actorPos, playerPos, storage); if (greetingTimer >= GREETING_SHOULD_END) { - greetingState = Greet_Done; + greetingState = AiWanderStorage::Greet_Done; greetingTimer = 0; } } - if (greetingState == MWMechanics::AiWander::Greet_Done) + if (greetingState == AiWanderStorage::Greet_Done) { float resetDist = 2 * helloDistance; if (playerDistSqr >= resetDist*resetDist) - greetingState = Greet_None; + greetingState = AiWanderStorage::Greet_None; } } @@ -676,7 +615,7 @@ namespace MWMechanics storage.mAllowedNodes.push_back(storage.mCurrentNode); storage.mCurrentNode = temp; - storage.setState(Wander_Walking); + storage.setState(AiWanderStorage::Wander_Walking); } // Choose a different node and delete this one from possible nodes because it is uncreachable: else diff --git a/apps/openmw/mwmechanics/aiwander.hpp b/apps/openmw/mwmechanics/aiwander.hpp index d96d93165..4a0811f50 100644 --- a/apps/openmw/mwmechanics/aiwander.hpp +++ b/apps/openmw/mwmechanics/aiwander.hpp @@ -21,8 +21,81 @@ namespace ESM } namespace MWMechanics -{ - struct AiWanderStorage; +{ + /// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive. + struct AiWanderStorage : AiTemporaryBase + { + // the z rotation angle to reach + // when mTurnActorGivingGreetingToFacePlayer is true + float mTargetAngleRadians; + bool mTurnActorGivingGreetingToFacePlayer; + float mReaction; // update some actions infrequently + + enum GreetingState + { + Greet_None, + Greet_InProgress, + Greet_Done + }; + GreetingState mSaidGreeting; + int mGreetingTimer; + + const MWWorld::CellStore* mCell; // for detecting cell change + + // AiWander states + enum WanderState + { + Wander_ChooseAction, + Wander_IdleNow, + Wander_MoveNow, + Wander_Walking + }; + WanderState mState; + + bool mIsWanderingManually; + bool mCanWanderAlongPathGrid; + + unsigned short mIdleAnimation; + std::vector mBadIdles; // Idle animations that when called cause errors + + // do we need to calculate allowed nodes based on mDistance + bool mPopulateAvailableNodes; + + // allowed pathgrid nodes based on mDistance from the spawn point + // in local coordinates of mCell + std::vector mAllowedNodes; + + ESM::Pathgrid::Point mCurrentNode; + bool mTrimCurrentNode; + + float mDoorCheckDuration; + int mStuckCount; + + AiWanderStorage(): + mTargetAngleRadians(0), + mTurnActorGivingGreetingToFacePlayer(false), + mReaction(0), + mSaidGreeting(Greet_None), + mGreetingTimer(0), + mCell(NULL), + mState(Wander_ChooseAction), + mIsWanderingManually(false), + mCanWanderAlongPathGrid(true), + mIdleAnimation(0), + mBadIdles(), + mPopulateAvailableNodes(true), + mAllowedNodes(), + mTrimCurrentNode(false), + mDoorCheckDuration(0), // TODO: maybe no longer needed + mStuckCount(0) + {}; + + void setState(const WanderState wanderState, const bool isManualWander = false) + { + mState = wanderState; + mIsWanderingManually = isManualWander; + } + }; /// \brief Causes the Actor to wander within a specified range class AiWander : public AiPackage @@ -52,19 +125,6 @@ namespace MWMechanics osg::Vec3f getDestination(const MWWorld::Ptr& actor) const; - enum GreetingState { - Greet_None, - Greet_InProgress, - Greet_Done - }; - - enum WanderState { - Wander_ChooseAction, - Wander_IdleNow, - Wander_MoveNow, - Wander_Walking - }; - private: // NOTE: mDistance and mDuration must be set already void init(); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index b4a820fcd..5e9f0c925 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -55,6 +55,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/player.hpp" +#include "aicombataction.hpp" #include "movement.hpp" #include "npcstats.hpp" #include "creaturestats.hpp" @@ -386,6 +387,10 @@ void CharacterController::refreshJumpAnims(const WeaponInfo* weap, JumpingState { jumpmask = MWRender::Animation::BlendMask_LowerBody; jumpAnimName = "jump"; + + // For crossbow animations use 1h ones as fallback + if (mWeaponType == WeapType_Crossbow) + jumpAnimName += "1h"; } } } @@ -434,11 +439,18 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character movementAnimName = movestate->groupname; if(weap != sWeaponTypeListEnd && movementAnimName.find("swim") == std::string::npos) { - movementAnimName += weap->shortgroup; + if (mWeaponType == WeapType_Spell && (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight)) // Spellcasting stance turning is a special case + movementAnimName = weap->shortgroup + movementAnimName; + else + movementAnimName += weap->shortgroup; if(!mAnimation->hasAnimation(movementAnimName)) { movemask = MWRender::Animation::BlendMask_LowerBody; movementAnimName = movestate->groupname; + + // For crossbow animations use 1h ones as fallback + if (mWeaponType == WeapType_Crossbow) + movementAnimName += "1h"; } } @@ -475,9 +487,11 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character } } - /* If we're playing the same animation, restart from the loop start instead of the - * beginning. */ - int mode = ((movementAnimName == mCurrentMovement) ? 2 : 1); + // If we're playing the same animation, start it from the point it ended + bool sameAnim = (movementAnimName == mCurrentMovement); + float startPoint = 0.f; + if (sameAnim) + mAnimation->getInfo(mCurrentMovement, &startPoint); mMovementAnimationControlled = true; @@ -526,7 +540,7 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character } mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, - 1.f, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul, true); + 1.f, (!sameAnim ? "start" : "loop start"), "stop", startPoint, ~0ul, true); } } } @@ -676,16 +690,19 @@ MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::I void CharacterController::playDeath(float startpoint, CharacterState death) { + // Make sure the character was swimming upon death for forward-compatibility + const bool wasSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr); + switch (death) { case CharState_SwimDeath: mCurrentDeath = "swimdeath"; break; case CharState_SwimDeathKnockDown: - mCurrentDeath = "swimdeathknockdown"; + mCurrentDeath = (wasSwimming ? "swimdeathknockdown" : "deathknockdown"); break; case CharState_SwimDeathKnockOut: - mCurrentDeath = "swimdeathknockout"; + mCurrentDeath = (wasSwimming ? "swimdeathknockout" : "deathknockout"); break; case CharState_DeathKnockDown: mCurrentDeath = "deathknockdown"; @@ -808,6 +825,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim , mSecondsOfRunning(0) , mTurnAnimationThreshold(0) , mAttackingOrSpell(false) + , mCastingManualSpell(false) , mTimeUntilWake(0.f) { if(!mAnimation) @@ -1021,7 +1039,8 @@ void CharacterController::handleTextKey(const std::string &groupname, const std: // the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type. && evt.compare(off, len, mAttackType + " release") == 0) { - MWBase::Environment::get().getWorld()->castSpell(mPtr); + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); + mCastingManualSpell = false; } else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) @@ -1102,7 +1121,9 @@ bool CharacterController::updateCreatureState() if (weapType == WeapType_Spell) { const std::string spellid = stats.getSpells().getSelectedSpell(); - if (!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); + + if (!spellid.empty() && canCast) { /* Start of tes3mp addition @@ -1123,7 +1144,7 @@ bool CharacterController::updateCreatureState() End of tes3mp addition */ - MWMechanics::CastSpell cast(mPtr, NULL); + MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid); /* @@ -1138,7 +1159,10 @@ bool CharacterController::updateCreatureState() */ if (!mAnimation->hasAnimation("spellcast")) - MWBase::Environment::get().getWorld()->castSpell(mPtr); // No "release" text key to use, so cast immediately + { + MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately + mCastingManualSpell = false; + } else { const ESM::Spell *spell = MWBase::Environment::get().getWorld()->getStore().get().find(spellid); @@ -1411,8 +1435,9 @@ bool CharacterController::updateWeaponState() stats.getSpells().setSelectedSpell(selectedSpell); } std::string spellid = stats.getSpells().getSelectedSpell(); + bool canCast = mCastingManualSpell || MWBase::Environment::get().getWorld()->startSpellCast(mPtr); - if(!spellid.empty() && MWBase::Environment::get().getWorld()->startSpellCast(mPtr)) + if(!spellid.empty() && canCast) { /* Start of tes3mp addition @@ -1433,7 +1458,7 @@ bool CharacterController::updateWeaponState() End of tes3mp addition */ - MWMechanics::CastSpell cast(mPtr, NULL); + MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell); cast.playSpellCastingEffects(spellid); const ESM::Spell *spell = store.get().find(spellid); @@ -1711,16 +1736,18 @@ bool CharacterController::updateWeaponState() break; } + // Note: apply reload animations only for upper body since blending with movement animations can give weird result. + // Especially noticable with crossbow reload animation. if(!start.empty()) { mAnimation->disable(mCurrentWeapon); if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, true, + MWRender::Animation::BlendMask_UpperBody, true, weapSpeed, start, stop, 0.0f, 0); else mAnimation->play(mCurrentWeapon, priorityWeapon, - MWRender::Animation::BlendMask_All, false, + MWRender::Animation::BlendMask_UpperBody, false, weapSpeed, start, stop, 0.0f, 0); } } @@ -2498,6 +2525,11 @@ bool CharacterController::isAttackPrepairing() const mUpperBodyState == UpperCharState_MinAttackToMaxAttack; } +bool CharacterController::isCastingSpell() const +{ + return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell; +} + bool CharacterController::isReadyToBlock() const { return updateCarriedLeftVisible(mWeaponType); @@ -2561,6 +2593,14 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell) mAttackingOrSpell = attackingOrSpell; } +void CharacterController::castSpell(const std::string spellId, bool manualSpell) +{ + mAttackingOrSpell = true; + mCastingManualSpell = manualSpell; + ActionSpell action = ActionSpell(spellId); + action.prepare(mPtr); +} + void CharacterController::setAIAttackType(const std::string& attackType) { mAttackType = attackType; diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 381cf71a5..84630a479 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -204,6 +204,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener std::string mAttackType; // slash, chop or thrust bool mAttackingOrSpell; + bool mCastingManualSpell; float mTimeUntilWake; @@ -276,6 +277,7 @@ public: void forceStateUpdate(); bool isAttackPrepairing() const; + bool isCastingSpell() const; bool isReadyToBlock() const; bool isKnockedDown() const; bool isKnockedOut() const; @@ -286,6 +288,7 @@ public: bool isAttackingOrSpell() const; void setAttackingOrSpell(bool attackingOrSpell); + void castSpell(const std::string spellId, bool manualSpell=false); void setAIAttackType(const std::string& attackType); static void setAttackTypeRandomly(std::string& attackType); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index cd7ca8b22..c19c6560a 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -266,6 +266,12 @@ namespace MWMechanics mObjects.addObject(ptr); } + void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) + { + if(ptr.getClass().isActor()) + mActors.castSpell(ptr, spellId, manualSpell); + } + void MechanicsManager::remove(const MWWorld::Ptr& ptr) { if(ptr == mWatched) @@ -1824,6 +1830,11 @@ namespace MWMechanics stats.resurrect(); } + bool MechanicsManager::isCastingSpell(const MWWorld::Ptr &ptr) const + { + return mActors.isCastingSpell(ptr); + } + bool MechanicsManager::isReadyToBlock(const MWWorld::Ptr &ptr) const { return mActors.isReadyToBlock(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index 4843e5f32..712a05a82 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -200,10 +200,14 @@ namespace MWMechanics virtual void keepPlayerAlive(); + virtual bool isCastingSpell (const MWWorld::Ptr& ptr) const; + virtual bool isReadyToBlock (const MWWorld::Ptr& ptr) const; /// Is \a ptr casting spell or using weapon now? virtual bool isAttackingOrSpell(const MWWorld::Ptr &ptr) const; + virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell=false); + /// Check if the target actor was detected by an observer /// If the observer is a non-NPC, check all actors in AI processing distance as observers virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer); diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 7ec2afbaf..d6a0d961e 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -332,13 +332,14 @@ namespace MWMechanics return true; } - CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile) + CastSpell::CastSpell(const MWWorld::Ptr &caster, const MWWorld::Ptr &target, const bool fromProjectile, const bool isScripted) : mCaster(caster) , mTarget(target) , mStack(false) , mHitPosition(0,0,0) , mAlwaysSucceed(false) , mFromProjectile(fromProjectile) + , mIsScripted(isScripted) { } @@ -961,7 +962,7 @@ namespace MWMechanics bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState(); - if (mCaster.getClass().isActor() && !mAlwaysSucceed) + if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mIsScripted) { school = getSpellSchool(spell, mCaster); @@ -1025,7 +1026,7 @@ namespace MWMechanics stats.getSpells().usePower(spell); } - if (mCaster == getPlayer() && spellIncreasesSkill(spell)) + if (mCaster == getPlayer() && spellIncreasesSkill()) mCaster.getClass().skillUsageSucceeded(mCaster, spellSchoolToSkill(school), 0); @@ -1149,6 +1150,14 @@ namespace MWMechanics } } + bool CastSpell::spellIncreasesSkill() + { + if (mIsScripted) + return false; + + return MWMechanics::spellIncreasesSkill(mId); + } + int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) { /* diff --git a/apps/openmw/mwmechanics/spellcasting.hpp b/apps/openmw/mwmechanics/spellcasting.hpp index 07c5b8477..5e69b4696 100644 --- a/apps/openmw/mwmechanics/spellcasting.hpp +++ b/apps/openmw/mwmechanics/spellcasting.hpp @@ -88,9 +88,10 @@ namespace MWMechanics osg::Vec3f mHitPosition; // Used for spawning area orb bool mAlwaysSucceed; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false) bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon) + bool mIsScripted; // True if spell is casted from script and ignores some checks (mana level, success chance, etc.) public: - CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false); + CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile=false, const bool isScripted=false); bool cast (const ESM::Spell* spell); @@ -108,6 +109,8 @@ namespace MWMechanics void playSpellCastingEffects(const std::string &spellid); + bool spellIncreasesSkill(); + /// Launch a bolt with the given effects. void launchMagicBolt (); diff --git a/apps/openmw/mwscript/containerextensions.cpp b/apps/openmw/mwscript/containerextensions.cpp index 88372eb76..ea45444c0 100644 --- a/apps/openmw/mwscript/containerextensions.cpp +++ b/apps/openmw/mwscript/containerextensions.cpp @@ -31,6 +31,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwworld/actionequip.hpp" #include "../mwworld/class.hpp" #include "../mwworld/containerstore.hpp" #include "../mwworld/inventorystore.hpp" @@ -215,12 +216,11 @@ namespace MWScript "to fulfil requirements of Equip instruction" << std::endl; } - if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) - MWBase::Environment::get().getWindowManager()->useItem(*it); + if (ptr == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->useItem(*it, true); else { - std::shared_ptr action = it->getClass().use(*it); - // No equip sound for actors other than the player + std::shared_ptr action = it->getClass().use(*it, true); action->execute(ptr, true); } } diff --git a/apps/openmw/mwscript/miscextensions.cpp b/apps/openmw/mwscript/miscextensions.cpp index c6decbf4a..f51ca9461 100644 --- a/apps/openmw/mwscript/miscextensions.cpp +++ b/apps/openmw/mwscript/miscextensions.cpp @@ -37,6 +37,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwmechanics/aicast.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/spellcasting.hpp" @@ -1153,15 +1154,31 @@ namespace MWScript { MWWorld::Ptr ptr = R()(runtime); - std::string spell = runtime.getStringLiteral (runtime[0].mInteger); + std::string spellId = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); std::string targetId = ::Misc::StringUtils::lowerCase(runtime.getStringLiteral (runtime[0].mInteger)); runtime.pop(); + const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get().find (spellId); + if (spell && spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power) + { + runtime.getContext().report("spellcasting failed: you can cast only spells and powers."); + return; + } + + // Obviously we can not use casting animation for player here + if (ptr.getClass().isActor() && ptr != MWMechanics::getPlayer()) + { + MWMechanics::AiCast castPackage(targetId, spellId, true); + ptr.getClass().getCreatureStats (ptr).getAiSequence().stack(castPackage, ptr); + + return; + } + MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr (targetId, false); - MWMechanics::CastSpell cast(ptr, target); + MWMechanics::CastSpell cast(ptr, target, false, true); cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); @@ -1179,7 +1196,7 @@ namespace MWScript std::string spell = runtime.getStringLiteral (runtime[0].mInteger); runtime.pop(); - MWMechanics::CastSpell cast(ptr, ptr); + MWMechanics::CastSpell cast(ptr, ptr, false, true); cast.mHitPosition = ptr.getRefData().getPosition().asVec3(); cast.mAlwaysSucceed = true; cast.cast(spell); diff --git a/apps/openmw/mwworld/actionalchemy.cpp b/apps/openmw/mwworld/actionalchemy.cpp index 53ed1ad84..37d811eb6 100644 --- a/apps/openmw/mwworld/actionalchemy.cpp +++ b/apps/openmw/mwworld/actionalchemy.cpp @@ -9,12 +9,19 @@ namespace MWWorld { + ActionAlchemy::ActionAlchemy(bool force) + : Action (false) + , mForce(force) + { + } + void ActionAlchemy::executeImp (const Ptr& actor) { if (actor != MWMechanics::getPlayer()) return; - if(MWMechanics::isPlayerInCombat()) { //Ensure we're not in combat + if(!mForce && MWMechanics::isPlayerInCombat()) + { //Ensure we're not in combat MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage3}"); return; } diff --git a/apps/openmw/mwworld/actionalchemy.hpp b/apps/openmw/mwworld/actionalchemy.hpp index e6d1a7976..0f79d5c2d 100644 --- a/apps/openmw/mwworld/actionalchemy.hpp +++ b/apps/openmw/mwworld/actionalchemy.hpp @@ -7,7 +7,11 @@ namespace MWWorld { class ActionAlchemy : public Action { - virtual void executeImp (const Ptr& actor); + bool mForce; + virtual void executeImp (const Ptr& actor); + + public: + ActionAlchemy(bool force=false); }; } diff --git a/apps/openmw/mwworld/actionequip.cpp b/apps/openmw/mwworld/actionequip.cpp index a6d6eef2b..da794bcd2 100644 --- a/apps/openmw/mwworld/actionequip.cpp +++ b/apps/openmw/mwworld/actionequip.cpp @@ -14,7 +14,9 @@ namespace MWWorld { - ActionEquip::ActionEquip (const MWWorld::Ptr& object) : Action (false, object) + ActionEquip::ActionEquip (const MWWorld::Ptr& object, bool force) + : Action (false, object) + , mForce(force) { } @@ -23,18 +25,29 @@ namespace MWWorld MWWorld::Ptr object = getTarget(); MWWorld::InventoryStore& invStore = actor.getClass().getInventoryStore(actor); - std::pair result = object.getClass().canBeEquipped (object, actor); + if (object.getClass().hasItemHealth(object) && object.getCellRef().getCharge() == 0) + { + if (actor == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage1}"); - // display error message if the player tried to equip something - if (!result.second.empty() && actor == MWMechanics::getPlayer()) - MWBase::Environment::get().getWindowManager()->messageBox(result.second); + return; + } - switch(result.first) + if (!mForce) { - case 0: - return; - default: - break; + std::pair result = object.getClass().canBeEquipped (object, actor); + + // display error message if the player tried to equip something + if (!result.second.empty() && actor == MWMechanics::getPlayer()) + MWBase::Environment::get().getWindowManager()->messageBox(result.second); + + switch(result.first) + { + case 0: + return; + default: + break; + } } // slots that this item can be equipped in diff --git a/apps/openmw/mwworld/actionequip.hpp b/apps/openmw/mwworld/actionequip.hpp index 3b56c7402..1b5e52dd8 100644 --- a/apps/openmw/mwworld/actionequip.hpp +++ b/apps/openmw/mwworld/actionequip.hpp @@ -2,17 +2,18 @@ #define GAME_MWWORLD_ACTIONEQUIP_H #include "action.hpp" -#include "ptr.hpp" namespace MWWorld { class ActionEquip : public Action { - virtual void executeImp (const Ptr& actor); + bool mForce; - public: - /// @param item to equip - ActionEquip (const Ptr& object); + virtual void executeImp (const Ptr& actor); + + public: + /// @param item to equip + ActionEquip (const Ptr& object, bool force=false); }; } diff --git a/apps/openmw/mwworld/actionrepair.cpp b/apps/openmw/mwworld/actionrepair.cpp index 191cf2063..4fb7f7ada 100644 --- a/apps/openmw/mwworld/actionrepair.cpp +++ b/apps/openmw/mwworld/actionrepair.cpp @@ -8,8 +8,9 @@ namespace MWWorld { - ActionRepair::ActionRepair(const Ptr &item) - : Action(false, item) + ActionRepair::ActionRepair(const Ptr& item, bool force) + : Action (false, item) + , mForce(force) { } @@ -18,7 +19,8 @@ namespace MWWorld if (actor != MWMechanics::getPlayer()) return; - if(MWMechanics::isPlayerInCombat()) { + if(!mForce && MWMechanics::isPlayerInCombat()) + { MWBase::Environment::get().getWindowManager()->messageBox("#{sInventoryMessage2}"); return; } diff --git a/apps/openmw/mwworld/actionrepair.hpp b/apps/openmw/mwworld/actionrepair.hpp index 1d3ef2bc1..fc64522d8 100644 --- a/apps/openmw/mwworld/actionrepair.hpp +++ b/apps/openmw/mwworld/actionrepair.hpp @@ -7,10 +7,13 @@ namespace MWWorld { class ActionRepair : public Action { - virtual void executeImp (const Ptr& actor); + bool mForce; + + virtual void executeImp (const Ptr& actor); public: - ActionRepair(const MWWorld::Ptr& item); + /// @param item repair hammer + ActionRepair(const Ptr& item, bool force=false); }; } diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 2ddfb1e55..d10283991 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -113,7 +113,7 @@ namespace MWWorld return std::shared_ptr (new NullAction); } - std::shared_ptr Class::use (const Ptr& ptr) const + std::shared_ptr Class::use (const Ptr& ptr, bool force) const { return std::shared_ptr (new NullAction); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index ebe160570..8dd2bd941 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -141,7 +141,7 @@ namespace MWWorld virtual std::shared_ptr activate (const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action). - virtual std::shared_ptr use (const Ptr& ptr) + virtual std::shared_ptr use (const Ptr& ptr, bool force=false) const; ///< Generate action for using via inventory menu (default implementation: return a /// null action). diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index ccc9d15b2..2cafb1af5 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -1299,7 +1299,10 @@ namespace MWWorld if (isPlayer) { if (!newCell->isExterior()) + { changeToInteriorCell(Misc::StringUtils::lowerCase(newCell->getCell()->mName), pos, false); + removeContainerScripts(getPlayerPtr()); + } else { if (mWorldScene->isCellActive(*newCell)) @@ -1320,7 +1323,8 @@ namespace MWWorld mWorldScene->addObjectToScene(newPtr); std::string script = newPtr.getClass().getScript(newPtr); - if (!script.empty()) { + if (!script.empty()) + { mLocalScripts.add(script, newPtr); } addContainerScripts(newPtr, newCell); @@ -3032,13 +3036,13 @@ namespace MWWorld return !fail; } - void World::castSpell(const Ptr &actor) + void World::castSpell(const Ptr &actor, bool manualSpell) { MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor); // For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result. std::vector targetActors; - if (!actor.isEmpty() && actor != MWMechanics::getPlayer()) + if (!actor.isEmpty() && actor != MWMechanics::getPlayer() && !manualSpell) actor.getClass().getCreatureStats(actor).getAiSequence().getCombatTargets(targetActors); const float fCombatDistance = getStore().get().find("fCombatDistance")->getFloat(); @@ -3056,51 +3060,71 @@ namespace MWWorld if (target.isEmpty()) { - // For actor targets, we want to use hit contact with bounding boxes. - // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. - // For object targets, we want the detailed shapes (rendering raycast). - // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. - std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); + // For scripted spells we should not use hit contact + if (manualSpell) + { + // Actors that are targeted by this actor's Follow or Escort packages also side with them + if (actor != MWMechanics::getPlayer()) + { + const MWMechanics::CreatureStats &stats = actor.getClass().getCreatureStats(actor); + for (std::list::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) + { + if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdCast) + { + target = (*it)->getTarget(); + break; + } + } + } + } + else + { + // For actor targets, we want to use hit contact with bounding boxes. + // This is to give a slight tolerance for errors, especially with creatures like the Skeleton that would be very hard to aim at otherwise. + // For object targets, we want the detailed shapes (rendering raycast). + // If we used the bounding boxes for static objects, then we would not be able to target e.g. objects lying on a shelf. + std::pair result1 = getHitContact(actor, fCombatDistance, targetActors); - // Get the target to use for "on touch" effects, using the facing direction from Head node - osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); + // Get the target to use for "on touch" effects, using the facing direction from Head node + osg::Vec3f origin = getActorHeadTransform(actor).getTrans(); - osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) - * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); + osg::Quat orient = osg::Quat(actor.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0)) + * osg::Quat(actor.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1)); - osg::Vec3f direction = orient * osg::Vec3f(0,1,0); - float distance = getMaxActivationDistance(); - osg::Vec3f dest = origin + direction * distance; + osg::Vec3f direction = orient * osg::Vec3f(0,1,0); + float distance = getMaxActivationDistance(); + osg::Vec3f dest = origin + direction * distance; - MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); + MWRender::RenderingManager::RayResult result2 = mRendering->castRay(origin, dest, true, true); - float dist1 = std::numeric_limits::max(); - float dist2 = std::numeric_limits::max(); + float dist1 = std::numeric_limits::max(); + float dist2 = std::numeric_limits::max(); - if (!result1.first.isEmpty() && result1.first.getClass().isActor()) - dist1 = (origin - result1.second).length(); - if (result2.mHit) - dist2 = (origin - result2.mHitPointWorld).length(); + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + dist1 = (origin - result1.second).length(); + if (result2.mHit) + dist2 = (origin - result2.mHitPointWorld).length(); - if (!result1.first.isEmpty() && result1.first.getClass().isActor()) - { - target = result1.first; - hitPosition = result1.second; - if (dist1 > getMaxActivationDistance()) - target = NULL; - } - else if (result2.mHit) - { - target = result2.mHitObject; - hitPosition = result2.mHitPointWorld; - if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) - target = NULL; + if (!result1.first.isEmpty() && result1.first.getClass().isActor()) + { + target = result1.first; + hitPosition = result1.second; + if (dist1 > getMaxActivationDistance()) + target = NULL; + } + else if (result2.mHit) + { + target = result2.mHitObject; + hitPosition = result2.mHitPointWorld; + if (dist2 > getMaxActivationDistance() && !target.isEmpty() && !target.getClass().canBeActivated(target)) + target = NULL; + } } } std::string selectedSpell = stats.getSpells().getSelectedSpell(); - MWMechanics::CastSpell cast(actor, target); + MWMechanics::CastSpell cast(actor, target, false, manualSpell); cast.mHitPosition = hitPosition; if (!selectedSpell.empty()) diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 630c3b222..06cfb162c 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -696,7 +696,7 @@ namespace MWWorld * @brief Cast the actual spell, should be called mid-animation * @param actor */ - void castSpell (const MWWorld::Ptr& actor) override; + void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) override; void launchMagicBolt (const std::string& spellId, const MWWorld::Ptr& caster, const osg::Vec3f& fallbackDirection) override; void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, diff --git a/cmake/OpenMWMacros.cmake b/cmake/OpenMWMacros.cmake index 6ccf18970..800c2e5e4 100644 --- a/cmake/OpenMWMacros.cmake +++ b/cmake/OpenMWMacros.cmake @@ -207,4 +207,4 @@ macro (copy_all_resource_files source_dir destination_dir_base destination_dir_r get_filename_component(filename ${f} NAME) copy_resource_file("${source_dir}/${f}" "${destination_dir_base}" "${destination_dir_relative}/${filename}") endforeach (f) -endmacro (copy_all_resource_files) \ No newline at end of file +endmacro (copy_all_resource_files) diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 225f431e2..3698659b6 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -210,6 +210,11 @@ add_component_dir (fallback fallback validate ) +if(NOT WIN32 AND NOT ANDROID) + add_component_dir (crashcatcher + crashcatcher + ) +endif() set (ESM_UI ${CMAKE_SOURCE_DIR}/files/ui/contentselector.ui ) diff --git a/apps/openmw/crashcatcher.cpp b/components/crashcatcher/crashcatcher.cpp similarity index 91% rename from apps/openmw/crashcatcher.cpp rename to components/crashcatcher/crashcatcher.cpp index 0078ef169..af42eb695 100644 --- a/apps/openmw/crashcatcher.cpp +++ b/components/crashcatcher/crashcatcher.cpp @@ -14,7 +14,11 @@ #include #include -#include +#include + +#include + +namespace bfs = boost::filesystem; #include @@ -402,7 +406,7 @@ static void crash_handler(const char *logfile) exit(0); } -int cc_install_handlers(int argc, char **argv, int num_signals, int *signals, const char *logfile, int (*user_info)(char*, char*)) +int crashCatcherInstallHandlers(int argc, char **argv, int num_signals, int *signals, const char *logfile, int (*user_info)(char*, char*)) { struct sigaction sa; stack_t altss; @@ -454,19 +458,31 @@ int cc_install_handlers(int argc, char **argv, int num_signals, int *signals, co return retval; } - -// gdb apparently opens FD(s) 3,4,5 (whereas a typical prog uses only stdin=0, stdout=1,stderr=2) -bool -is_debugger_attached(void) +static bool is_debugger_present() { - bool rc = false; - FILE *fd = fopen("/tmp", "r"); - - if (fileno(fd) > 5) + bfs::ifstream file((bfs::path("/proc/self/status"))); + while (!file.eof()) { - rc = true; + std::string word; + file >> word; + if (word == "TracerPid:") + { + file >> word; + return word != "0"; + } } + return false; +} - fclose(fd); - return rc; +void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath) +{ + if ((argc == 2 && strcmp(argv[1], "--cc-handle-crash") == 0) || !is_debugger_present()) + { + int s[5] = { SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGABRT }; + if (crashCatcherInstallHandlers(argc, argv, 5, s, crashLogPath.c_str(), NULL) == -1) + { + std::cerr << "Installing crash handler failed" << std::endl; + } else + std::cout << "Crash handler installed" << std::endl; + } } diff --git a/components/crashcatcher/crashcatcher.hpp b/components/crashcatcher/crashcatcher.hpp new file mode 100644 index 000000000..fd8f0d154 --- /dev/null +++ b/components/crashcatcher/crashcatcher.hpp @@ -0,0 +1,20 @@ +#ifndef CRASHCATCHER_H +#define CRASHCATCHER_H + +#include + +#if (defined(__APPLE__) || (defined(__linux) && !defined(ANDROID)) || (defined(__unix) && !defined(ANDROID)) || defined(__posix)) + #define USE_CRASH_CATCHER 1 +#else + #define USE_CRASH_CATCHER 0 +#endif + +#if USE_CRASH_CATCHER +extern void crashCatcherInstall(int argc, char **argv, const std::string &crashLogPath); +#else +inline void crashCatcherInstall(int, char **, const std::string &crashLogPath) +{ +} +#endif + +#endif diff --git a/components/nif/nifstream.cpp b/components/nif/nifstream.cpp index 08a7466fa..3811d05ee 100644 --- a/components/nif/nifstream.cpp +++ b/components/nif/nifstream.cpp @@ -4,10 +4,24 @@ namespace Nif { + osg::Quat NIFStream::getQuaternion() + { + float f[4]; + readLittleEndianBufferOfType<4, float,uint32_t>(inp, (float*)&f); + osg::Quat quat; + quat.w() = f[0]; + quat.x() = f[1]; + quat.y() = f[2]; + quat.z() = f[3]; + return quat; + } -//Private functions - - -//Public functions - + Transformation NIFStream::getTrafo() + { + Transformation t; + t.pos = getVector3(); + t.rotation = getMatrix3(); + t.scale = getFloat(); + return t; + } } diff --git a/components/nif/nifstream.hpp b/components/nif/nifstream.hpp index d00069be7..0fdf4b440 100644 --- a/components/nif/nifstream.hpp +++ b/components/nif/nifstream.hpp @@ -80,8 +80,8 @@ template type inline readLittleEndianType(File return val; } -class NIFStream { - +class NIFStream +{ /// Input stream Files::IStreamPtr inp; @@ -93,71 +93,71 @@ public: void skip(size_t size) { inp->ignore(size); } - char getChar() + char getChar() { - return readLittleEndianType(inp); + return readLittleEndianType(inp); } - short getShort() - { + + short getShort() + { return readLittleEndianType(inp); } - unsigned short getUShort() - { + + unsigned short getUShort() + { return readLittleEndianType(inp); } - int getInt() + + int getInt() { return readLittleEndianType(inp); } - unsigned int getUInt() - { + + unsigned int getUInt() + { return readLittleEndianType(inp); } - float getFloat() - { + + float getFloat() + { return readLittleEndianType(inp); } - osg::Vec2f getVector2() { + osg::Vec2f getVector2() + { osg::Vec2f vec; readLittleEndianBufferOfType<2,float,uint32_t>(inp, (float*)&vec._v[0]); return vec; } - osg::Vec3f getVector3() { + + osg::Vec3f getVector3() + { osg::Vec3f vec; readLittleEndianBufferOfType<3, float,uint32_t>(inp, (float*)&vec._v[0]); return vec; } - osg::Vec4f getVector4() { + + osg::Vec4f getVector4() + { osg::Vec4f vec; readLittleEndianBufferOfType<4, float,uint32_t>(inp, (float*)&vec._v[0]); return vec; } - Matrix3 getMatrix3() { + + Matrix3 getMatrix3() + { Matrix3 mat; readLittleEndianBufferOfType<9, float,uint32_t>(inp, (float*)&mat.mValues); return mat; } - osg::Quat getQuaternion() { - float f[4]; - readLittleEndianBufferOfType<4, float,uint32_t>(inp, (float*)&f); - osg::Quat quat; - quat.w() = f[0]; - quat.x() = f[1]; - quat.y() = f[2]; - quat.z() = f[3]; - return quat; - } - Transformation getTrafo() { - Transformation t; - t.pos = getVector3(); - t.rotation = getMatrix3(); - t.scale = getFloat(); - return t; - } + + osg::Quat getQuaternion(); + + Transformation getTrafo(); ///Read in a string of the given length - std::string getString(size_t length) { + std::string getString(size_t length) + { std::vector str(length + 1, 0); inp->read(&str[0], length); @@ -165,41 +165,54 @@ public: return &str[0]; } ///Read in a string of the length specified in the file - std::string getString() { + std::string getString() + { size_t size = readLittleEndianType(inp); return getString(size); } ///This is special since the version string doesn't start with a number, and ends with "\n" - std::string getVersionString() { + std::string getVersionString() + { std::string result; std::getline(*inp, result); return result; } - void getUShorts(std::vector &vec, size_t size) { + void getUShorts(std::vector &vec, size_t size) + { vec.resize(size); readLittleEndianDynamicBufferOfType(inp, &vec.front(), size); } - void getFloats(std::vector &vec, size_t size) { + + void getFloats(std::vector &vec, size_t size) + { vec.resize(size); readLittleEndianDynamicBufferOfType(inp, &vec.front(), size); } - void getVector2s(std::vector &vec, size_t size) { + + void getVector2s(std::vector &vec, size_t size) + { vec.resize(size); /* The packed storage of each Vec2f is 2 floats exactly */ readLittleEndianDynamicBufferOfType(inp,(float*) &vec.front(), size*2); } - void getVector3s(std::vector &vec, size_t size) { + + void getVector3s(std::vector &vec, size_t size) + { vec.resize(size); /* The packed storage of each Vec3f is 3 floats exactly */ readLittleEndianDynamicBufferOfType(inp, (float*) &vec.front(), size*3); } - void getVector4s(std::vector &vec, size_t size) { + + void getVector4s(std::vector &vec, size_t size) + { vec.resize(size); /* The packed storage of each Vec4f is 4 floats exactly */ readLittleEndianDynamicBufferOfType(inp, (float*) &vec.front(), size*4); } - void getQuaternions(std::vector &quat, size_t size) { + + void getQuaternions(std::vector &quat, size_t size) + { quat.resize(size); for (size_t i = 0;i < quat.size();i++) quat[i] = getQuaternion(); diff --git a/components/nifbullet/bulletnifloader.cpp b/components/nifbullet/bulletnifloader.cpp index 8f827e4e2..be5a7d9d6 100644 --- a/components/nifbullet/bulletnifloader.cpp +++ b/components/nifbullet/bulletnifloader.cpp @@ -34,6 +34,13 @@ btVector3 getbtVector(const osg::Vec3f &v) return btVector3(v.x(), v.y(), v.z()); } +bool pathFileNameStartsWithX(const std::string& path) +{ + const std::size_t slashpos = path.find_last_of("/\\"); + const std::size_t letterPos = slashpos == std::string::npos ? 0 : slashpos + 1; + return letterPos < path.size() && (path[letterPos] == 'x' || path[letterPos] == 'X'); +} + } namespace NifBullet @@ -88,18 +95,10 @@ osg::ref_ptr BulletNifLoader::load(const Nif::NIFFilePtr& else { bool autogenerated = hasAutoGeneratedCollision(node); - bool isAnimated = false; // files with the name convention xmodel.nif usually have keyframes stored in a separate file xmodel.kf (see Animation::addAnimSource). // assume all nodes in the file will be animated - std::string filename = nif->getFilename(); - size_t slashpos = filename.find_last_of("/\\"); - if (slashpos == std::string::npos) - slashpos = 0; - if (slashpos+1 < filename.size() && (filename[slashpos+1] == 'x' || filename[slashpos+1] == 'X')) - { - isAnimated = true; - } + const bool isAnimated = pathFileNameStartsWithX(nif->getFilename()); handleNode(node, 0, autogenerated, isAnimated, autogenerated); diff --git a/components/resource/stats.cpp b/components/resource/stats.cpp index bf34d5534..0a70d75ab 100644 --- a/components/resource/stats.cpp +++ b/components/resource/stats.cpp @@ -10,6 +10,8 @@ #include #include +#include + namespace Resource { @@ -19,7 +21,7 @@ StatsHandler::StatsHandler(): _statsType(false), _statsWidth(1280.0f), _statsHeight(1024.0f), - _font("fonts/arial.ttf"), + _font(""), _characterSize(20.0f) { _camera = new osg::Camera; @@ -28,6 +30,15 @@ StatsHandler::StatsHandler(): _camera->setProjectionResizePolicy(osg::Camera::FIXED); _resourceStatsChildNum = 0; + + _font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); +} + +Profiler::Profiler() +{ + _font = osgMyGUI::DataManager::getInstance().getDataPath("DejaVuLGCSansMono.ttf"); + + setKeyEventTogglesOnScreenStats(osgGA::GUIEventAdapter::KEY_F3); } bool StatsHandler::handle(const osgGA::GUIEventAdapter &ea, osgGA::GUIActionAdapter &aa) @@ -77,7 +88,6 @@ void StatsHandler::setWindowSize(int width, int height) { _camera->setProjectionMatrix(osg::Matrix::ortho2D(0.0,_statsWidth,_statsHeight-height*_statsWidth/width,_statsHeight)); } - } void StatsHandler::toggle(osgViewer::ViewerBase *viewer) diff --git a/components/resource/stats.hpp b/components/resource/stats.hpp index 55b016602..8f04ee10d 100644 --- a/components/resource/stats.hpp +++ b/components/resource/stats.hpp @@ -1,7 +1,7 @@ #ifndef OPENMW_COMPONENTS_RESOURCE_STATS_H #define OPENMW_COMPONENTS_RESOURCE_STATS_H -#include +#include namespace osgViewer { @@ -15,6 +15,11 @@ namespace osg namespace Resource { + class Profiler : public osgViewer::StatsHandler + { + public: + Profiler(); + }; class StatsHandler : public osgGA::GUIEventHandler { diff --git a/docs/requirements.txt b/docs/requirements.txt index 72c2aaa12..288d462d0 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ parse_cmake -sphinx +sphinx>=1.7.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 0ba8567c0..8905044dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,7 @@ sys.path.insert(0, project_root) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +needs_sphinx = '1.7' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -34,8 +34,11 @@ extensions = [ 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', + 'sphinx.ext.autosectionlabel', ] +#autosectionlabel_prefix_document = True + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 3781733cb..de3773ddb 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,11 +1,9 @@ Welcome to OpenMW's Documentation! ================================== -Sections --------- - .. toctree:: - :maxdepth: 3 + :caption: Table of Contents + :maxdepth: 3 - manuals/index - reference/index + manuals/index + reference/index diff --git a/docs/source/manuals/installation/common-problems.rst b/docs/source/manuals/installation/common-problems.rst new file mode 100644 index 000000000..543b8ec9b --- /dev/null +++ b/docs/source/manuals/installation/common-problems.rst @@ -0,0 +1,50 @@ +############### +Common Problems +############### + +ERROR: Unknown fallback name: FontColor_color_header +#################################################### + +:Symptoms: + OpenMW crashes at startup with + ``ERROR: Unknown fallback name: FontColor_color_header`` + message at the end of ``openmw.log``, located `here `_. + +:Cause: + The OpenMW `configuration file `_ ``openmw.cfg`` + is severely lacking and missing fallback values + because "Settings Importer" was not run correctly. + +:Fix: + Re-run "Settings Importer" from the OpenMW launcher. + +Installing game files via Steam on macOS: DISK WRITE ERROR +########################################################## + +:Symptoms: + Steam stages the download for Morrowind, but does not proceed. + The download will read "Paused: DISK WRITE ERROR". + +:Cause: + The OpenMW `configuration file `_ ``openmw.cfg`` + is severely lacking and missing fallback values + because "Settings Importer" was not run correctly. + +:Fix: + Open appmanifest_22320.acf in your favorite text editor. + Locate or create an entry under the "StateFlags" entry titled "installdir" + and give it the value "Morrowind". + Your file should now look something like this:: + + "AppState" + { + "appid" "22320" + "Universe" "1" + "name" "The Elder Scrolls III: Morrowind" + "StateFlags" "4" + "installdir" "Morrowind" + + [other entries] + } + + Restart the Steam client. The download should now proceed. \ No newline at end of file diff --git a/docs/source/manuals/installation/index.rst b/docs/source/manuals/installation/index.rst index f22e77449..6e6f5034e 100644 --- a/docs/source/manuals/installation/index.rst +++ b/docs/source/manuals/installation/index.rst @@ -1,3 +1,4 @@ +################## Installation Guide ################## @@ -8,4 +9,4 @@ In order to use OpenMW, you must install both the engine and the game files for install-openmw install-game-files - + common-problems \ No newline at end of file diff --git a/docs/source/manuals/installation/install-game-files.rst b/docs/source/manuals/installation/install-game-files.rst index 11c746f44..ac3688e88 100644 --- a/docs/source/manuals/installation/install-game-files.rst +++ b/docs/source/manuals/installation/install-game-files.rst @@ -1,5 +1,126 @@ -================== +################## Install Game Files -================== +################## + +OpenMW is a complete game engine that can either run `Morrowind`_ +or original projects created with OpenMW-CS, such as `Example Suite`_. + +Morrowind +######### + +Running the Morrowind Installation Wizard +========================================= + +#. Launch the OpenMW Launcher +#. Launch the Installation Wizard + + .. note:: + If you are prompted with an error message stating + "Could not find the Data Files location," + click the "Run Installation Wizard" button. + .. note:: + If you arrive at the main screen, click the "Settings" tab, + and then click the "Run Installation Wizard" button. + +#. Follow further instructions below + to install Morrowind from either a retail CD or an existing installation. + + - **Morrowind (from retail CD)** + + #. Make sure that the retail CD is in your computer's CD/DVD drive + and the Installation Wizard is running. + #. On the "Select Installation Method" screen of the Installation Wizard, + choose "Install Morrowind to a New Location" and click the "Next" button. + #. Choose a location to install Morrowind to your hard drive + (or accept the suggested location) and click the "Next" button. + #. Select your preferred language for the installation + and click the "Next" button + #. Select which official expansions (Tribunal or Bloodmoon) should be installed. + For best results, it is recommended to have both expansions installed. + #. Click the "Install" button. + + - **Morrowind (from existing installation)** + + #. On the "Select Installation Method" screen of the Installation Wizard, + choose "Select an existing Morrowind installation" and click the "Next" button + #. Select an installation. If nothing is detected, click browse. + #. Navigate to the directory containing the file ``Morrowind.esm`` and select that file. + +#. You will be asked if you wish to import settings from ``Morrowind.ini``. + Select "Import", otherwise OpenMW will not work. + (You do not need to check the box "Include selected masters and plugins"). +#. The OpenMW launcher window should now open. + Switch to the "Data Files" tab and check the box to the left of ``Morrowind.esm``. +#. You are now ready to play! + +Installing Morrowind +==================== + +----------------- +Retail CD and GOG +----------------- + +Windows users can run the installer if they haven't already. +By default, both Bethesda's official installer on the retail CD +and the GOG installer install to ``C:\Program Files\Bethesda Softworks\Morrowind``. +You will find ``Morrowind.esm`` there. + +Users of other platforms running Wine, will find it at +``~/.wine/drive_c/Program Files/Bethesda Softworks/Morrowind`` + +----- +Steam +----- + +Windows +------- + +Windows users can download Morrowind through Steam. +Afterwards, you can point OpenMW to the Steam install location at +``C:\Program Files\Steam\SteamApps\common\Morrowind\Data Files\`` +and find ``Morrowind.esm`` there. + +macOS +----- + +If you are running macOS, you can also download Morrowind through Steam: + +#. Navigate to ``/Users/YOUR_USERNAME_HERE/Library/Application Support/Steam/steamapps/`` +#. Create a file called ``appmanifest_22320.acf`` + (the number is based on its `Steam App ID `_). + If using TextEdit, + make sure that your document is in plain text mode by going to the menu bar + and choosing "Format" -> "Make Plain Text". + Also, ensure that it's not named with the extension ``.acf.txt``. + Add the following into that file:: + + "AppState" + { + "AppID" "22320" + "Universe" "1" + "StateFlags" "1026" + "installdir" "The Elder Scrolls III - Morrowind" + } + +#. Launch the Steam client and let it download. You can then find ``Morrowind.esm`` at + ``~/Library/Application Support/Steam/steamapps/common/The Elder Scrolls III - Morrowind/Data Files/`` + +Wine +---- + +Users of other platforms running Wine can run Steam within it +and find ``Morrowind.esm`` at +``~/.wine/drive_c/Program Files/Steam/SteamApps/common/Morrowind/Data Files/``. + +Example Suite +############# + +Example Suite is a demo showing the capabilities of the OpenMW engine. +At this time, it requires Morrowind to be installed to run, +but does not use any assets from it. +In the future, it will be possible to run without installing Morrowind first. + +#. `Install Morrowind `_ +#. `Download the latest version `_ +#. Follow the platform-specific instructions in the zip file's ``Installation.md`` file. -Coming Soon... \ No newline at end of file diff --git a/docs/source/reference/documentationHowTo.rst b/docs/source/reference/documentationHowTo.rst index 2d8b4fae2..b27c122cb 100644 --- a/docs/source/reference/documentationHowTo.rst +++ b/docs/source/reference/documentationHowTo.rst @@ -12,6 +12,32 @@ The premise of this guide is that you would like to help out the OpenMW project *However*, as much as I will try to guide you through all the tedious setup and day-to-day stuff, you will eventually have to learn to write using ReST (reStructuredText) formatting. Since you're probably like me when I started helping and don't know wtf ReST is, never fear. It's an incredibly simple language that is easy to read in plain text form that can then be converted automatically into different types of documents like PDFs and html for webpages. +Baby Steps +========== + +Create an account on GitLab (https://gitlab.com), and sign in. +(Github probably works too, though some details will differ. More details later – maybe.) + +Go to the OpenMW project on GitLab (https://gitlab.com/OpenMW/openmw) +Navigate to whatever documentation you want to tackle. +Choose Repository and Files in the menu on the left, then docs and source in the tree in the centre. + +Don’t overlook the tutorial-style-guide.txt there for some tips to get you started. + +Open whichever file you want to tackle – probably within the manuals or reference directories. +There’s also a dropdown box to the right of edit, at the top of the left menu, which offers options such as new file or directory, or upload file, with “+” to close that dropdown box. + +Click on "Edit" towards top right which will reveal the underlying version, rather than the version displayed to normal reaaders. Use "Write" and "Preview" to switch between the two views. + +When you have made the appropriate changes, and checked them in Preview mode, click the Green "Commit changes" button at the bottom. +This should add a branch, with a default name such as patch-1, to your own repository, and add a Merge Request to the main OpenMW Project. + +[More details on the commit and review process] + +Changes between submission and acceptance of the Merge Request - just make them in your branch, then press the Commit button there. + +Going Deeper +============ So here's what you're gonna be learning how to set up: 1. `GitHub`_ @@ -103,6 +129,33 @@ You will probably find it helpful to be able to preview any documentation you've Now in order to generate the documentation on your computer to preview them, just click the green play button in the top right, next to the drop down menu with the name you chose above selected. Sphinx will run and you can view the resulting documentation wherever you chose Output to be, above. The window that Sphinx runs in will also show any errors that occur during the build in red, which should help you find typos and missing/incorrect syntax. +GitLab integration in PyCharm +============================= + +As most of the hosting of OpenMW has moved to Gitlab, we should encourage the use of GitLab, though GitHub will continue to be supported. + +Add a couple of plugins to Pycharm - see general instructions at https://www.jetbrains.com/help/pycharm/installing-updating-and-uninstalling-repository-plugins.html + +For Linux/Windows - (Macos is a little different) + +1. File/Settings/Plugins +2. Browse Repositories +3. Filter with “GitLab” +4. Install “GitLab Integration Plugin”, +5. Follow the accompanying instructions to register your GitLab account (after restarting PyCharm) - File/Settings/Other Settings/Gitlab Integration +6. Install “GitLab Projects” + +Within your account on GitLab + +1. Fork OpenMW if you haven’t already done so +2. Select Settings from the dropdown box in your Avatar (top right) +3. Select Access Tokens from the list on the left +4. Enter a name for application that will use it – say “PyCharm” +5. Set an expiry date +6. Check the “api” box +7. Create the token, and use it to complete the setup of the "GitLab Integration Plugin" above. + + Sample PR ========= diff --git a/docs/source/reference/index.rst b/docs/source/reference/index.rst index 3f2e2f560..cd947745e 100644 --- a/docs/source/reference/index.rst +++ b/docs/source/reference/index.rst @@ -1,8 +1,9 @@ +################## Reference Material -================== +################## .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - modding/index - documentationHowTo \ No newline at end of file + modding/index + documentationHowTo \ No newline at end of file diff --git a/docs/source/reference/modding/index.rst b/docs/source/reference/modding/index.rst index d2dd90850..30793504e 100644 --- a/docs/source/reference/modding/index.rst +++ b/docs/source/reference/modding/index.rst @@ -8,19 +8,19 @@ about creating new content for OpenMW, please refer to :doc:`/manuals/openmw-cs/index`. .. warning:: - OpenMW is still software in development. This manual does not cover any - of its shortcomings. It is written as if everything was working as - intended. Please report any software problems as bugs in the software, - not errors in the manual. + OpenMW is still software in development. This manual does not cover any + of its shortcomings. It is written as if everything was working as + intended. Please report any software problems as bugs in the software, + not errors in the manual. .. toctree:: - :caption: Table of Contents - :maxdepth: 2 + :caption: Table of Contents + :maxdepth: 2 - foreword - differences - mod-install - settings/index - font - convert_bump_mapped_mods - paths + foreword + differences + mod-install + settings/index + texture-modding/index + font + paths diff --git a/docs/source/reference/modding/convert_bump_mapped_mods.rst b/docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst similarity index 100% rename from docs/source/reference/modding/convert_bump_mapped_mods.rst rename to docs/source/reference/modding/texture-modding/convert-bump-mapped-mods.rst diff --git a/docs/source/reference/modding/texture-modding/index.rst b/docs/source/reference/modding/texture-modding/index.rst new file mode 100644 index 000000000..3e0b359ee --- /dev/null +++ b/docs/source/reference/modding/texture-modding/index.rst @@ -0,0 +1,16 @@ +###################### +OpenMW Texture Modding +###################### + +Although texture modding requires external tools not supported by the OpenMW team, +adding and modifying textures is an important part of content creation. +Therefore, we've included the following in hopes of helping modders transition +to texture modding in OpenMW. + +.. toctree:: + :caption: Table of Contents + :maxdepth: 2 + + texture-basics + convert-bump-mapped-mods + native-mesh-format diff --git a/docs/source/reference/modding/texture-modding/native-mesh-format.rst b/docs/source/reference/modding/texture-modding/native-mesh-format.rst new file mode 100644 index 000000000..c2ddd26ba --- /dev/null +++ b/docs/source/reference/modding/texture-modding/native-mesh-format.rst @@ -0,0 +1,114 @@ +################## +Native Mesh Format +################## + +This article explains how to export a model from Blender to OpenMW using the OSG model format. +Starting with OpenMW version 0.38 we can utilize the OSG native model format. +The OSG model format doesn't yet support all the features that NIF's support, +but works for basic models. For more details on the format, refer to +`this forum post `_. + +Previously, NIF files were the only way to get models into the game. +Unfortunately, the NIF format is proprietary, bloated, +and the available exporters are not in great shape. +For example, the Blender NIF exporter currently only works with the very old Blender 2.49. + +Prerequisites +############# + +- OpenMW 0.38 or later +- Blender 2.60 or later +- OSG exporter add-on for Blender +- A Blender model you would like to export + +Installing the exporter +####################### + +#. Download the + `OSG export script + `_ +#. Open Blender and go to File -> User Preferences +#. Select Add-ons -> Install from File, then select the downloaded `.zip` +#. Enter "osg" into the search bar, then tick the checkbox next to the add-on to enable it +#. Now click Save user setting so the exporter stays enabled when you re-start Blender + +You can now export your models using the OSG model (osgt) entry in the File -> Export menu. + +Model's location +################ + +The model needs to be at 0,0,0 coordinates in Blender, +as this is where its origin will always be when exported. +If the model is offset from 0,0,0 in Blender, +it will be offset from its origin in the exported file as well. + +Model's rotation +################ + +- Blender's Z axis is up axis in OpenMW +- Blender's Y axis is front axis in OpenMW +- Blender's X axis is left-right axis in OpenMW +- Visual rotation is taken into account when exporting + +Model's scale +############# + +Blender:OpenMW model scale is 70:1, +which means 70 Blender units (BU) translate into 1m in OpenMW. +Using this scale, any models you make will fit with the existing ones. +The scale does not need to be applied, +the exporter will always use the visual scale of the model. +However, 70 is an odd number to work with so here's an alternative workflow: + +- In Blender, use a scale of 1BU = 1m which is a nice scale and ratio to work with. + Have all models use this scale. +- Before exporting a model, scale it up by a factor of 70. +- After exporting, undo the model's scale change and continue working as normal + (in the future a preferable way would be to apply the scale through the exporter) + +Putting the model in-game +######################### + +Place the exported model in the Meshes sub-folder of a data folder recognized by the game, +e.g. the Morrowind Data Files folder, or the local data folder. +Place all required textures in the Textures sub-folder. +Now start OpenMW-CS, create a new addon file +and you should see your mesh in the Assets -> Meshes table. +Go ahead and make some object use your mesh. +You can now preview by right clicking on the object -> Preview to see what the mesh will look like. + +Converting the model to binary +############################## + +When the model behaves to our liking, +we can think about converting it from the "osgt" text format to the "osgb" binary format +so that it's smaller in size - and thus faster to load. + +To do this, simply invoke the osgconv tool. +This tool should be included with your distribution of OpensceneGraph. + +`osgconv -O WriteImageHint=UseExternal model.osgt model.osgb` + +Of course, you can convert the binary model back to text format as well: + +`osgconv -O WriteImageHint=UseExternal model.osgb model.osgt` + +Note the use of `-O WriteHint=UseExternal` option. +Enabling this option is desirable because it keeps the textures as external file references, +rather than embedding the textures within the model file. +Embedded textures have disadvantages such as being hard to inspect, +and impossible to share between model files. + +Using shaders/normal maps +######################### + +See :ref:`OSG Native Files` + +Conclusion +########## + +These are the basics of getting a textured, static model from Blender into the game. +In the future, we will want a way to add texture animations, +skeletal animations, separate collision shapes, +and some other features that are currently only available via NIF files. +We will likely add these features to the native OSG format after OpenMW 1.0. \ No newline at end of file diff --git a/docs/source/reference/modding/texture-modding/texture-basics.rst b/docs/source/reference/modding/texture-modding/texture-basics.rst new file mode 100644 index 000000000..d9137c89d --- /dev/null +++ b/docs/source/reference/modding/texture-modding/texture-basics.rst @@ -0,0 +1,169 @@ +autosectionlabel_prefix_document = True + +###################### +Texture Modding Basics +###################### + +OpenMW supports new texture mapping techniques +that were not available in the vanilla Morrowind engine. + +`Normal mapping`_ is a technique used to fake lighting of bumps, +cracks and other small details. + +`Specular mapping`_ is used to vary the shininess/specularity along the surface of an object. + +The prerequisite to using these techniques are +`shaders `_. +OpenMW automatically uses shaders for objects with these mapping techniques. + +Normal Mapping +############## + +To plug in a normal map, you name the normal map as the diffuse texture but with a specified suffix after. OpenMW will then recognise the file and load it as a normal map, provided you have set up your settings file correctly. See the section `Automatic use`_ further down below for detailed information. + +.. note:: + While the original Morrowind engine does support the loading of a BumpTexture slot in the NIF, it will not display it as a normal map. Morrowind Code Patch (MCP) added a way to hack normal maps into the engine by first enabling the engine to load the BumpTexture slot as an environment map and then turn down the brightness of the environment map. This will imitate how a real normal map shader would display a normal map, but it will not look exactly the same. + OpenMW uses standard normal mapping, which achieves much better results. + Unfortunately, this difference can result in incompatibilities. + Some mods + (e.g. `Redoran Bump Mapped `_) + look much darker compared to the vanilla engine and will have to be recalibrated. + +Specular Mapping +################ + +The RGB channels of the specular map are used as the specular color. +The alpha channel specifies shininess in range [0, 255]. +If a specular map is used, it will override the shininess and specular color +set in the NiMaterialProperty / osg::Material. + +NIF files do not support specular maps. +In order to use them anyway, see the next section. + +Automatic Use +############# + +In addition to editing mesh files, +there is another way of plugging in these texture maps. +Simply create the textures with appropriate naming convention +(e.g. when the base texture is called foo.dds, +the normal map would have to be called foo_n.dds). +To enable this automatic use based on filename pattern, +you will have to add the following to your +`settings.cfg `_ file:: + + [Shaders] + auto use object normal maps = true + + auto use object specular maps = true + + normal map pattern = _n + normal height map pattern = _nh + + specular map pattern = _spec + +Additionally, a normal map with the `_nh` pattern enables +the use of the normal map's alpha channel as height information. +This can be used by a `parallax mapping `_ +shader to offset the texture depending on the viewing angle and height, +creating a fake 3D effect. + +The above settings are not enabled by default to prevent incompatibilities +with mods that may be inadvertently using these naming schemes. + +On the topic of shader settings, +you may be interested in these three settings as well: :ref:`force shaders`, +:ref:`force per pixel lighting`, and :ref:`clamp lighting`. +In particular, `clamp lighting = false` makes normal maps look much better! + +Terrain +####### + +The terrain shader also supports normal, normal-height and specular maps, +with one difference compared to objects: +the specular value must be packed into the layer texture's alpha channel. + +For example, if you wanted to add specular mapping to a terrain layer called rock.dds, +you would copy this texture to a new file called rock_diffusespec.dds, +and then edit its alpha channel to set the specular intensity. + +The relevant settings are:: + + [Shaders] + auto use terrain normal maps = true + + auto use terrain specular maps = true + + terrain specular map pattern = _diffusespec + + # Also used for terrain normal maps + normal map pattern = _n + normal height map pattern = _nh + +OSG native files +################ + +OpenMW supports all the above shader features for meshes in the Native Mesh Format. +To have the shader generator recognize specific textures, +the `osg::Texture2D` must be named appropriately. + +Available texture types are the following (most of which also have NIF equivalents): + +:diffuseMap: base texture +:normalMap: normal map, as described earlier +:normalHeightMap: same as normal map, but including height information in alpha channel to be used for parallax effects +:emissiveMap: controls the material's emission, useful for objects that glow in the dark +:darkMap: multiplied onto the base texture +:detailMap: multiplied by 2 and then multiplied onto the base texture +:envMap: spherical environment map +:specularMap: specular map, as described earlier + +The first texture unit automatically acts as diffuseMap if no recognized type is specified. + +Example: `.osgt` file excerpt of a normal mapped mesh:: + + TextureModeList 2 { + Data 1 { + GL_TEXTURE_2D ON + } + Data 1 { + GL_TEXTURE_2D ON + } + } + TextureAttributeList 2 { + Data 1 { + osg::Texture2D { + UniqueID 37 + Name "diffuseMap" + WRAP_S REPEAT + WRAP_T REPEAT + WRAP_R REPEAT + MIN_FILTER LINEAR_MIPMAP_LINEAR + MAG_FILTER LINEAR + Image TRUE { + UniqueID 60 + FileName "textures/BuddhaStatue_Dif.jpg" + WriteHint 2 2 + } + } + Value OFF + } + Data 1 { + osg::Texture2D { + UniqueID 38 + Name "normalMap" + WRAP_S REPEAT + WRAP_T REPEAT + WRAP_R REPEAT + MIN_FILTER LINEAR_MIPMAP_LINEAR + MAG_FILTER LINEAR + Image TRUE { + UniqueID 61 + FileName "textures/BuddhaStatue_Nor.jpg" + WriteHint 2 2 + } + } + Value OFF + } + } +